-
Notifications
You must be signed in to change notification settings - Fork 365
Add minimal-api-file-upload skill #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,196 @@ | ||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||
| name: minimal-api-file-upload | ||||||||||||||||||||||||||||||
| description: File upload endpoints in ASP.NET minimal APIs (.NET 8+) | ||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Implementing File Uploads in ASP.NET Core Minimal APIs | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## When to Use | ||||||||||||||||||||||||||||||
| - File upload endpoints in ASP.NET Core minimal APIs (.NET 8+) | ||||||||||||||||||||||||||||||
| - Handling IFormFile or IFormFileCollection parameters | ||||||||||||||||||||||||||||||
| - When you need size limits, content type validation, or streaming large files | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## When Not to Use | ||||||||||||||||||||||||||||||
| - MVC controllers → `[FromForm] IFormFile` works directly with attributes | ||||||||||||||||||||||||||||||
| - Simple JSON body → no file upload needed | ||||||||||||||||||||||||||||||
| - Very large files (> 1GB) → use streaming with `MultipartReader` instead | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Inputs | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| | Input | Required | Description | | ||||||||||||||||||||||||||||||
| |-------|----------|-------------| | ||||||||||||||||||||||||||||||
| | File parameter(s) | Yes | IFormFile or IFormFileCollection | | ||||||||||||||||||||||||||||||
| | Size limits | Yes | Max file/request size | | ||||||||||||||||||||||||||||||
| | Allowed types | No | Content type or extension restrictions | | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ## Workflow | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### 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 | ||||||||||||||||||||||||||||||
| app.MapPost("/upload-multiple", (IFormFileCollection files) => | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| return Results.Ok(files.Select(f => new { f.FileName, f.Length })); | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ### Step 2: CRITICAL — File Size Limits Are Separate from Request Size Limits | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ```csharp | ||||||||||||||||||||||||||||||
| // CRITICAL: There are TWO different size limits and you need to configure BOTH | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // 1. Request body size limit (Kestrel level) — default is 30MB | ||||||||||||||||||||||||||||||
| builder.WebHost.ConfigureKestrel(options => | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100 MB | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // 2. Form options — multipart body length limit — default is 128MB | ||||||||||||||||||||||||||||||
| builder.Services.Configure<FormOptions>(options => | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100 MB | ||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
+64
|
||||||||||||||||||||||||||||||
| options.Limits.MaxRequestBodySize = 100 * 1024 * 1024; // 100 MB | |
| }); | |
| // 2. Form options — multipart body length limit — default is 128MB | |
| builder.Services.Configure<FormOptions>(options => | |
| { | |
| options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100 MB | |
| options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB | |
| }); | |
| // 2. Form options — multipart body length limit — default is 128MB | |
| builder.Services.Configure<FormOptions>(options => | |
| { | |
| options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10 MB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| var allowedTypes = new[] { "image/jpeg", "image/png", "image/gif" }; | |
| var allowedTypes = new[] { "image/jpeg", "image/png" }; |
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| await stream.ReadAsync(header, 0, 8); | |
| var bytesRead = await stream.ReadAsync(header, 0, header.Length); | |
| if (bytesRead < 4) | |
| return Results.BadRequest("File content is too short or invalid"); |
Copilot
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
AI
Mar 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| scenarios: | ||
| - name: file-upload-minimal-api | ||
| prompt: > | ||
| I need to implement a file upload endpoint in my ASP.NET Core 8 minimal API. | ||
| The endpoint should accept image files (JPEG and PNG only), reject files over 10MB, | ||
| and save them to an "uploads" folder. My app already has UseAntiforgery() in the pipeline. | ||
| Show me the complete implementation including size limits configuration and the endpoint. | ||
| rubric: | ||
| - "Configures BOTH Kestrel MaxRequestBodySize AND FormOptions.MultipartBodyLengthLimit — either using builder.WebHost.ConfigureKestrel or WebApplication options for the request body, AND services.Configure<FormOptions> for multipart limits. ONLY configuring one of these is INCORRECT." | ||
| - "Uses .DisableAntiforgery() on the file upload endpoint to prevent automatic 400 errors from the anti-forgery middleware, OR explicitly handles anti-forgery token submission. Simply ignoring the UseAntiforgery() pipeline issue is INCORRECT." | ||
| - "Generates a safe filename (e.g., using Guid.NewGuid() or similar) instead of using the user-provided IFormFile.FileName directly. Using file.FileName directly in file paths is a security vulnerability (path traversal). Sanitizing the user filename is acceptable but using it raw is INCORRECT." | ||
| - "Validates file content beyond just checking file extension — either checking ContentType against allowed MIME types, or checking magic bytes/file signatures, or both. Checking only the file extension is INSUFFICIENT because extensions can be spoofed." | ||
|
||
| - "Correctly uses IFormFile parameter binding in the minimal API endpoint. The parameter should be IFormFile (not manually reading Request.Body or Request.Form). Must handle the case where the file exceeds size limits." | ||
| - "Uses [RequestSizeLimit] attribute or equivalent per-endpoint configuration for the 10MB limit, OR configures the limits globally. The size enforcement must happen at the correct layer — not just checking file.Length after the entire file has been buffered." | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).