Best for
- 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
dotnet/skills/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md
File upload endpoints in ASP.NET minimal APIs (.NET 8+)
Decision brief
File upload endpoints in ASP. NET minimal APIs (.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-aspnetcore/skills/minimal-api-file-upload"Inspect the Agent Skill "minimal-api-file-upload" from https://github.com/dotnet/skills/blob/1b896e91feb0f613cb54a914f1efd2897810ae02/plugins/dotnet-aspnetcore/skills/minimal-api-file-upload/SKILL.md at commit 1b896e91feb0f613cb54a914f1efd2897810ae02. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Review the “Workflow” section in the pinned source before continuing.
Review the “Step 1: CRITICAL — Understand IFormFile Binding in Minimal APIs” section in the pinned source before continuing.
Review the “Step 2: CRITICAL — File Size Limits Are Separate from Request Size Limits” section in the pinned source before continuing.
Review the “Step 3: CRITICAL — Anti-Forgery Auto-Validates Form Uploads in .NET 8+” section in the pinned source before continuing.
Review the “Step 4: CRITICAL — Validate File Content, Not Just Extension” section in the pinned source before continuing.
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 90/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 5,248 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
[FromForm] IFormFile works directly with attributesMultipartReader instead| 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 |
// In .NET 8+ minimal APIs, IFormFile binds automatically from multipart/form-data
// when it is the only complex parameter.
app.MapPost("/upload", (IFormFile file) => ...);
// CRITICAL: When you mix files with other form fields, use [FromForm] on all
// form-bound parameters (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 });
});
// 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.
app.MapPost("/upload-multiple", (IFormFileCollection files) =>
{
return Results.Ok(files.Select(f => new { f.FileName, f.Length }));
});
// 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 = 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
options.ValueLengthLimit = 1024 * 1024; // 1 MB for form values
options.MultipartHeadersLengthLimit = 16384; // 16 KB for section headers
});
// COMMON MISTAKE: Only increasing Kestrel MaxRequestBodySize
// upload still fails because FormOptions.MultipartBodyLengthLimit is exceeded
// COMMON MISTAKE: Only increasing FormOptions
// upload fails with "Request body too large" from Kestrel before reaching form parsing
// CRITICAL: Per-endpoint override with RequestSizeLimit attribute
app.MapPost("/upload-large", [RequestSizeLimit(200_000_000)] (IFormFile file) =>
{
return Results.Ok(new { file.FileName, file.Length });
});
// CRITICAL: To disable the limit entirely (for streaming):
app.MapPost("/upload-unlimited", [DisableRequestSizeLimit] async (HttpContext context) =>
{
// Handle manually
});
// CRITICAL: In .NET 8+ with UseAntiforgery(), ALL form-bound endpoints
// automatically validate anti-forgery tokens, INCLUDING file uploads
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// This endpoint now REQUIRES an anti-forgery token:
app.MapPost("/upload", (IFormFile file) => Results.Ok(file.FileName));
// Without the token → 400 Bad Request
// CRITICAL: For API-only file uploads (no anti-forgery needed), opt out:
app.MapPost("/api/upload", (IFormFile file) => Results.Ok(file.FileName))
.DisableAntiforgery(); // CRITICAL: Must explicitly opt out
// COMMON MISTAKE: Getting 400 errors on file uploads and not realizing
// it's because UseAntiforgery() is in the pipeline
// WARNING: DisableAntiforgery() is safe for unauthenticated endpoints and
// endpoints using JWT bearer authentication. However, for endpoints
// authenticated with cookies, disabling antiforgery removes CSRF protection
// and exposes the endpoint to cross-site request forgery attacks.
// For cookie-authenticated endpoints, include a valid antiforgery token instead.
app.MapPost("/upload", async (IFormFile file) =>
{
// CRITICAL: Check content type AND file signature (magic bytes)
// NEVER trust file extension alone — it can be spoofed
// 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" };
if (!allowedTypes.Contains(file.ContentType, StringComparer.OrdinalIgnoreCase))
return Results.BadRequest("File type not allowed");
// CRITICAL: Check magic bytes for file type verification
using var stream = file.OpenReadStream();
var header = new byte[8];
var bytesRead = await stream.ReadAsync(header, 0, header.Length);
if (bytesRead < 4)
return Results.BadRequest("File content is too short or invalid");
// JPEG: FF D8 FF
// PNG: 89 50 4E 47
var isJpeg = header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF;
var isPng = header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47;
// Determine the actual content type from magic bytes
string? detectedContentType = isJpeg ? "image/jpeg" : isPng ? "image/png" : null;
if (detectedContentType is null)
return Results.BadRequest("File content is not a supported image format (only JPEG and PNG are allowed).");
// Ensure the declared Content-Type matches what the magic bytes detected
if (!string.Equals(file.ContentType, detectedContentType, StringComparison.OrdinalIgnoreCase))
return Results.BadRequest("File content type does not match the declared ContentType header.");
// CRITICAL: Never use the user-provided filename directly for the save path — it can
// contain path traversal characters (e.g., "../../../etc/passwd").
// Generate a safe filename; derive the extension from validated content, not user input.
var extension = detectedContentType == "image/jpeg" ? ".jpg" : ".png";
var safeFileName = $"{Guid.NewGuid()}{extension}";
// NEVER: var path = Path.Combine("uploads", file.FileName); // Path traversal!
var filePath = Path.Combine("uploads", safeFileName);
Directory.CreateDirectory("uploads");
stream.Position = 0;
using var fileStream = File.Create(filePath);
await stream.CopyToAsync(fileStream);
return Results.Ok(new { FileName = safeFileName, file.Length });
});
// CRITICAL: IFormFile relies on multipart form parsing that buffers content in memory
// (up to a threshold) then spills to temp files on disk. For very large uploads,
// this overhead is unnecessary if you can process the data in chunks.
// Use MultipartReader to stream directly — e.g., to a final storage location —
// without buffering the entire file first.
app.MapPost("/upload-stream",
[DisableRequestSizeLimit]
async (HttpContext context) =>
{
// Extract the multipart boundary from the Content-Type header
var contentType = context.Request.ContentType;
if (contentType == null)
return Results.BadRequest("Missing Content-Type");
// Safely parse the Content-Type header to avoid FormatException from MediaTypeHeaderValue.Parse
if (!MediaTypeHeaderValue.TryParse(contentType, out var mediaType))
return Results.BadRequest("Invalid Content-Type");
var boundary = HeaderUtilities.RemoveQuotes(mediaType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
return Results.BadRequest("Not a multipart request");
var reader = new MultipartReader(boundary, context.Request.Body);
// CRITICAL: ReadNextSectionAsync returns null when there are no more sections
while (await reader.ReadNextSectionAsync() is { } section)
{
// Parse Content-Disposition to identify file sections
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition))
continue;
if (contentDisposition.DispositionType.Equals("form-data")
&& !string.IsNullOrEmpty(contentDisposition.FileName.Value))
{
// Sanitize the user-provided filename to prevent path traversal
var originalFileName = contentDisposition.FileName.Value ?? string.Empty;
var sanitizedFileName = Path.GetFileName(originalFileName.Trim('"'));
var safeFile = $"{Guid.NewGuid()}";
// CRITICAL: Stream directly to disk — avoids buffering in memory
Directory.CreateDirectory("uploads");
using var fileStream = File.Create(Path.Combine("uploads", safeFile));
await section.Body.CopyToAsync(fileStream);
}
}
return Results.Ok("Uploaded");
}).DisableAntiforgery();
// COMMON MISTAKE: Using IFormFile for very large files
// Multipart form parsing can buffer large uploads and consume memory/disk.
// Use MultipartReader for streaming directly to storage.
MaxRequestBodySize AND FormOptions.MultipartBodyLengthLimit.UseAntiforgery() auto-validates form uploads. Use .DisableAntiforgery() for API endpoints (safe for JWT/unauthenticated; do NOT disable for cookie-authenticated endpoints).Guid.NewGuid() and derive the extension from validated content.MultipartReader to stream data in chunks directly to storage without buffering the entire file.Path.GetExtension(file.FileName). If the original extension must be preserved, validate it against the detected content type.Frequently asked questions
File upload endpoints in ASP. NET minimal APIs (.
The source record exposes this install command: npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-aspnetcore/skills/minimal-api-file-upload". Inspect the command and pinned source before running it.
Alternatives
garrytan/gbrain
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing
oaustegard/claude-skills
Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre