Skip to content

Add minimal-api-file-upload skill#264

Open
mrsharm wants to merge 1 commit intodotnet:mainfrom
mrsharm:musharm/implementing-form-file-uploads-minimal-apis
Open

Add minimal-api-file-upload skill#264
mrsharm wants to merge 1 commit intodotnet:mainfrom
mrsharm:musharm/implementing-form-file-uploads-minimal-apis

Conversation

@mrsharm
Copy link
Member

@mrsharm mrsharm commented Mar 6, 2026

Summary

Adds the minimal-api-file-upload skill for handling file uploads in ASP.NET Core 8 minimal APIs.

Note: Replaces #155 (migrated from skills-old repo to new plugins/ structure).

Eval Results (3-run)

  • Overall: +38.9% improvement (BL=3.0, SK=5.0)

Files

  • plugins/dotnet/skills/minimal-api-file-upload/SKILL.md
  • tests/dotnet/minimal-api-file-upload/eval.yaml

@mrsharm
Copy link
Member Author

mrsharm commented Mar 6, 2026

Migration Note

This PR replaces #155 which was opened from mrsharm/skills-old. The skill and eval files have been migrated to the new plugins/ directory structure:

  • src/dotnet/skills/minimal-api-file-upload/plugins/dotnet/skills/minimal-api-file-upload/
  • src/dotnet/tests/minimal-api-file-upload/tests/dotnet/minimal-api-file-upload/

All prior review feedback from #155 still applies — please see that PR for the full discussion history.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new .NET skill and evaluation scenario covering correct file-upload handling patterns for ASP.NET Core 8 minimal APIs (size limits, antiforgery, safe filenames, and content validation), and assigns code ownership for the new content.

Changes:

  • Added minimal-api-file-upload skill documentation under plugins/dotnet/skills/.
  • Added a new evaluation scenario under tests/dotnet/ for the skill.
  • Updated .github/CODEOWNERS to include owners for the new skill and its tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
plugins/dotnet/skills/minimal-api-file-upload/SKILL.md New skill guidance for file uploads in .NET 8 minimal APIs (binding, limits, antiforgery, validation, streaming).
tests/dotnet/minimal-api-file-upload/eval.yaml New eval scenario and rubric for minimal API file upload handling.
.github/CODEOWNERS Adds ownership entries for the new skill and its evals.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +173 to +174
var fileName = contentDisposition.FileName.Value;
var safeFile = $"{Guid.NewGuid()}{Path.GetExtension(fileName)}";
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the streaming example, contentDisposition.FileName.Value is used directly to compute the extension. This value can include paths and other unexpected content; using it directly reintroduces path traversal / spoofing risks. Prefer extracting a safe base name first (e.g., Path.GetFileName(...) + quote removal) and, ideally, deriving the output extension from validated content rather than the client-provided filename.

Suggested change
var fileName = contentDisposition.FileName.Value;
var safeFile = $"{Guid.NewGuid()}{Path.GetExtension(fileName)}";
var originalFileName = contentDisposition.FileName.Value ?? string.Empty;
var sanitizedFileName = Path.GetFileName(originalFileName.Trim('"'));
var safeFile = $"{Guid.NewGuid()}{Path.GetExtension(sanitizedFileName)}";

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +153
// CRITICAL: IFormFile buffers the entire file in memory by default
// For large files, use MultipartReader for streaming
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text claims IFormFile “buffers the entire file in memory by default”. ASP.NET Core typically buffers form bodies with a memory threshold and spills to disk, so the guidance here is misleading. Consider rephrasing to describe the real concern (multipart/form parsing buffers and can be expensive; for very large uploads prefer streaming with MultipartReader / disabling form binding) and, if relevant, mention the key knobs (FormOptions.MemoryBufferThreshold, temp file location, etc.).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +43
### Step 1: CRITICAL — IFormFile Requires [FromForm] in Minimal APIs (Not Automatic)

```csharp
// COMMON MISTAKE: Expecting IFormFile to bind automatically
app.MapPost("/upload", (IFormFile file) => ...);
// In early .NET versions, this worked differently. In .NET 8:

// CRITICAL: IFormFile IS bound automatically from form data in .NET 8
// BUT when you mix IFormFile with other parameters, you need [FromForm]
app.MapPost("/upload-with-metadata",
([FromForm] IFormFile file, [FromForm] string description) =>
{
return Results.Ok(new { file.FileName, Description = description });
});

// CRITICAL: For multiple files, use IFormFileCollection
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Step 1 heading/example says it's a “COMMON MISTAKE” to expect IFormFile to bind automatically, but immediately below it says IFormFile IS bound automatically from form data in .NET 8. This is internally inconsistent and will confuse readers about when [FromForm] is required. Please rewrite this section to clearly state the real rule (e.g., IFormFile binds from multipart/form-data by default when it's the only complex parameter, but mixing with other parameters generally requires [FromForm] on all form-bound parameters), and align the examples accordingly.

Suggested change
### Step 1: CRITICAL — IFormFile Requires [FromForm] in Minimal APIs (Not Automatic)
```csharp
// COMMON MISTAKE: Expecting IFormFile to bind automatically
app.MapPost("/upload", (IFormFile file) => ...);
// In early .NET versions, this worked differently. In .NET 8:
// CRITICAL: IFormFile IS bound automatically from form data in .NET 8
// BUT when you mix IFormFile with other parameters, you need [FromForm]
app.MapPost("/upload-with-metadata",
([FromForm] IFormFile file, [FromForm] string description) =>
{
return Results.Ok(new { file.FileName, Description = description });
});
// CRITICAL: For multiple files, use IFormFileCollection
### Step 1: CRITICAL — Understand IFormFile Binding in Minimal APIs
```csharp
// Simple case: a single IFormFile parameter binds automatically from multipart/form-data
app.MapPost("/upload", (IFormFile file) => ...);
// In early .NET versions, this worked differently. In .NET 8 minimal APIs:
// IFormFile (and IFormFileCollection) are bound automatically when they are the only complex parameters.
// BUT when you mix files with other form fields, be explicit and use [FromForm] on all form-bound parameters.
app.MapPost("/upload-with-metadata",
([FromForm] IFormFile file, [FromForm] string description) =>
{
return Results.Ok(new { file.FileName, Description = description });
});
// Multiple files: IFormFileCollection also binds automatically from multipart/form-data

Copilot uses AI. Check for mistakes.
// CRITICAL: Check content type AND file signature (magic bytes)
// NEVER trust file extension alone — it can be spoofed

var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif" };
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowlist includes image/gif, but the eval scenario and the surrounding guidance emphasize JPEG/PNG-only uploads. Including GIF here risks training the model to accept additional types by default. Consider limiting the example to JPEG/PNG and mentioning how to extend the allowlist when needed.

Suggested change
var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif" };
// Allow only JPEG/PNG by default. To support more (e.g., GIF),
// add the MIME type here AND validate its magic bytes below.
var allowedTypes = new[] { "image/jpeg", "image/png" };

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +138
// CRITICAL: Generate a safe filename — never use user-provided filename directly
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
// NEVER: var path = Path.Combine("uploads", file.FileName); // Path traversal!
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeFileName uses Path.GetExtension(file.FileName), which is attacker-controlled. Even with ContentType/magic-byte validation, this can still produce unsafe or misleading extensions (e.g., saving a JPEG as .exe). Prefer deriving the extension from the validated file signature/content type (mapping jpeg→.jpg, png→.png) rather than trusting the user-provided name for any part of the output path.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +161
var boundary = context.Request.GetMultipartBoundary();
if (string.IsNullOrEmpty(boundary))
return Results.BadRequest("Not a multipart request");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snippet calls context.Request.GetMultipartBoundary(), but that isn't a standard ASP.NET Core API and there’s no helper shown/linked in the skill. As written, readers following this will hit a compile error. Please either include the helper/extension implementation in the skill (and ensure it handles quoted boundaries and length limits), or switch the snippet to use the supported pattern from Microsoft.AspNetCore.WebUtilities samples (e.g., extracting the boundary from the Content-Type header with HeaderUtilities).

Copilot uses AI. Check for mistakes.
@mrsharm
Copy link
Member Author

mrsharm commented Mar 6, 2026

Feedback carried over from #155

Code Review Comments

Copilot on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 43): Step 1 is internally contradictory: it’s titled as if IFormFile requires [FromForm] (not automatic), but later in this section it states IFormFile is bound automatically in .NET 8. Please rewrite this section to present one clear rule (e.g., IFormFile binds from multipart automatically, but mixed parameters should be annotated with [FromForm] or grouped). suggestion ### Step 1: CRITICAL — Understand IFormFile Binding and [FromForm] in Minimal APIs csharp // In .NET 8 minimal APIs, IFormFile is bound automatically from multipart/form-data // when it is the only body parameter. app.MapPost("/upload", (IFormFile file) => ...); // When you mix files with other form fields, annotate the body-bound parameters // with [FromForm] (or group them into a single [FromForm] DTO). app.MapPost("/upload-with-metadata", ([FromForm] IFormFile file, [FromForm] string description) => { return Results.Ok(new { file.FileName, Description = description }); }); // For multiple files, IFormFileCollection also binds automatically from multipart/form-data. // You only need [FromForm] if you mix it with other form fields, as shown above. --- **Copilot** on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 138): The “safe filename” still uses `Path.GetExtension(file.FileName)`, which derives the extension from user-controlled input. Since the skill emphasizes not trusting client metadata, prefer choosing the extension based on the validated signature (map magic bytes to ".jpg"/".png") or omit the original extension entirely. --- **Copilot** on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 163): This example uses `context.Request.GetMultipartBoundary()`, but that helper isn’t defined anywhere in this repo and isn’t a built-in ASP.NET Core API. Please include the helper implementation (or switch to the standard boundary parsing approach) so the snippet is complete/compilable. --- **Copilot** on src/dotnet/tests/minimal-api-file-upload/eval.yaml (line 12): Rubric criterion allows validating only `ContentType` (“either checking ContentType… or checking magic bytes…”), but the PR description explicitly calls out ContentType as client-spoofable and emphasizes magic-byte validation as a key gotcha. Tighten this rubric to require signature/magic-byte checking (at least for JPEG/PNG) so the eval enforces the intended lesson. --- **Copilot** on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 64): These examples set the global Kestrel request body limit to 100MB, which can be confusing given the scenario is about enforcing a 10MB maximum. Consider using 10MB in the sample (or explicitly explain why the global limit is higher than the per-endpoint limit).suggestion options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB }); // 2. Form options — multipart body length limit — default is 128MB builder.Services.Configure(options => { options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10 MB --- **Copilot** on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 125): When reading magic bytes, the code ignores the return value from ReadAsync. For empty/short files this leaves parts of `header` as zeroes and can misclassify content. Capture the bytes-read and fail fast if fewer than the required bytes are available (e.g., < 4 for PNG, < 3 for JPEG).suggestion var bytesRead = await stream.ReadAsync(header, 0, header.Length); if (bytesRead < 4) return Results.BadRequest("File content is too short or invalid"); --- **Copilot** on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 118): The allowed MIME types include `image/gif`, but the scenario/rubric for this skill is JPEG + PNG only. Including GIF here makes the example inconsistent with the stated requirements and may cause models to permit additional types.suggestion var allowedTypes = new[] { "image/jpeg", "image/png" }; ``` --- Copilot on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 153): The statement that “IFormFile buffers the entire file in memory by default” is inaccurate for ASP.NET Core: multipart parsing uses buffering with a memory threshold and typically spills to a temp file. Please reword to the actual risk (request/form parsing can buffer large uploads and consume memory/disk) so the guidance is technically correct. --- Copilot on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 172): section.GetContentDispositionHeader() / `contentDisposition.IsFileDisposition()` also aren’t built-in APIs and aren’t defined in this repo. Either provide these helpers or show the canonical parsing with `ContentDispositionHeaderValue` so readers can use the sample as-is. --- timheuer on src/dotnet/skills/implementing-form-file-uploads-minimal-apis/SKILL.md (line N/A): This feels too verbose a name -- "minimal-api-file-upload"? --- timheuer on src/dotnet/skills/implementing-form-file-uploads-minimal-apis/SKILL.md (line N/A): Any validation that "8" is going to influence too much? --- timheuer on src/dotnet/skills/implementing-form-file-uploads-minimal-apis/SKILL.md (line N/A): Strike "8" and put more information in the 'when to use'? (note below) --- timheuer on src/dotnet/skills/implementing-form-file-uploads-minimal-apis/SKILL.md (line N/A): - File upload endpoings in ASP.NET minimal APIs (.NET 8+) --- halter73 on src/dotnet/skills/minimal-api-file-upload/SKILL.md (line 108): @GrabYourPitchforks @blowdart This should be okay for unauthenticated endpoints and endpoints using JWT bearer authentication, but I worry that this might cause people to disable antiforgery for endpoints authenticated with cookies. I wonder what the best way to communicate this potential security threat in the skill document. ---

Discussion Comments

mrsharm: ## Eval Results: implementing-form-file-uploads-minimal-apis ### 3-Run Validation: +38.9% PASS | Metric | Value | |--------|-------| | Overall Improvement | +38.9% | | Confidence Interval | [+9.1%, +62.5%] significant | | Effect Size (g) | +100.0% | | Baseline Quality | 3.0/5 | | Skill Quality | 5.0/5 | | Quality Delta | +2.0 | | Task Completion | Baseline: Pass, Skill: Pass | | Token Usage | +51.7% (164K -> 250K) | | Tool Calls | -16.7% (18 -> 15) | ### Baseline Analysis (BL=3) The baseline gets the basics right but consistently misses: - Only configures one of the two required size limits (Kestrel MaxRequestBodySize OR FormOptions.MultipartBodyLengthLimit, but not both) - Relies on ContentType alone for validation (client-spoofable) - Uses user-provided filenames without sanitization ### Skill Impact With skill loaded, the model correctly: - Configures BOTH Kestrel and FormOptions size limits - Calls DisableAntiforgery() on upload endpoints - Generates safe filenames with GUIDs - Validates file content with magic bytes - Uses proper IFormFile binding patterns Model: claude-opus-4.6 (baseline + skill), claude-opus-4.6 (judge) --- ViktorHofer: As discussed offline in the "dotnet/skills content" chat, this PR will need to be re-submitted from a connected fork. Also please update this PR based on the new repo folder structure (plugins instead of src). ---

@mrsharm
Copy link
Member Author

mrsharm commented Mar 6, 2026

/evaluate

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

Skill Validation Results

Skill Scenario Baseline With Skill Δ Skills Loaded Overfit Verdict
minimal-api-file-upload Implement file upload endpoint with validation 3.3/5 5.0/5 +1.7 ✅ minimal-api-file-upload; tools: skill, report_intent, view, glob, bash, edit ✅ 0.15
minimal-api-file-upload Fix 400 error on file upload with UseAntiforgery 4.0/5 5.0/5 +1.0 ✅ minimal-api-file-upload; tools: report_intent, skill ✅ 0.15
minimal-api-file-upload Dual size limit configuration 4.0/5 3.3/5 -0.7 ✅ minimal-api-file-upload; tools: skill ✅ 0.15
minimal-api-file-upload File upload should not use magic bytes for JSON API 5.0/5 5.0/5 0.0 ℹ️ not activated (expected) ✅ 0.15

Model: claude-opus-4.6 | Judge: claude-opus-4.6

Full results

@ViktorHofer
Copy link
Member

File upload should not use magic bytes for JSON API

Same feedback as in the other PRs regarding the skill not getting activated. If intentional, add expect_activation: false

@mrsharm
Copy link
Member Author

mrsharm commented Mar 8, 2026

/evaluate

@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

✅ Evaluation completed. View results | View workflow run

@ManishJayaswal
Copy link
Contributor

@mrsharm - - the repo has undergone some restructuring to make everything more organized. Hence, we are asking all open PRs to update the branch. Sorry about this.
This skill should be under ASP plugin. Please update the PR and submit again.
@adityamandaleeka @BrennanConroy - please review

@mrsharm mrsharm force-pushed the musharm/implementing-form-file-uploads-minimal-apis branch from d7e4ba5 to 4db7a40 Compare March 10, 2026 20:18
Copilot AI review requested due to automatic review settings March 10, 2026 20:18
@mrsharm mrsharm force-pushed the musharm/implementing-form-file-uploads-minimal-apis branch from 4db7a40 to fa88f24 Compare March 10, 2026 20:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…cenarios

Per repo restructuring feedback, ASP.NET Core specific skills should
be under the aspnetcore plugin rather than the dotnet plugin.
@mrsharm mrsharm force-pushed the musharm/implementing-form-file-uploads-minimal-apis branch from fa88f24 to 6a58890 Compare March 10, 2026 20:44
@mrsharm
Copy link
Member Author

mrsharm commented Mar 10, 2026

/evaluate

@github-actions
Copy link
Contributor

Skill Validation Results

Skill Scenario Quality Skills Loaded Overfit Verdict
minimal-api-file-upload Implement secure file upload in ASP.NET Core 8 minimal API 4.7/5 → 5.0/5 🟢 ✅ minimal-api-file-upload; tools: skill ✅ 0.11
minimal-api-file-upload Fix file upload returning 400 Bad Request 4.3/5 ⏰ → 3.3/5 ⏰ 🔴 ✅ minimal-api-file-upload; tools: skill ✅ 0.11
minimal-api-file-upload Upload multiple files with metadata in minimal API 5.0/5 → 5.0/5 ✅ minimal-api-file-upload; tools: skill ✅ 0.11

timeout — run hit the scenario timeout limit; scoring may be impacted by aborting model execution before it could produce its full output

Model: claude-opus-4.6 | Judge: claude-opus-4.6

Full results

@danmoseley
Copy link
Member

If #207 creates a dotnet-aspnet plugin, this skill should presumably move there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants