Skip to content
Development
Skill

/minimal-api-file-upload

File upload endpoints in ASP.NET minimal APIs (.NET 8+)

From plugin
dotnet-skills
5.1k96 skills16 agents
Install
$ npx -y skills add dotnet/skills --skill minimal-api-file-upload --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/minimal-api-file-upload

Context preview

The summary Claude sees to decide when to auto-load this skill.

File upload endpoints in ASP.NET minimal APIs (.NET 8+)

SKILL.md

minimal-api-file-upload.SKILL.md
name: minimal-api-file-upload
description: File upload endpoints in ASP.NET minimal APIs (.NET 8+)
license: MIT

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 — Understand IFormFile Binding in Minimal APIs

// 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 }));
});

Step 2: CRITICAL — File Size Limits Are Separate from Request Size Limits

// 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
});

Step 3: CRITICAL — Anti-Forgery Auto-Validates Form Uploads in .NET 8+

// 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.

Step 4: CRITICAL — Validate File Content, Not Just Extension

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 matc
Read more
Ships withdotnet-skills

This repository contains the .NET team's curated set of core skills and custom agents for coding agents. For information about the Agent Skills standard, see agentskills.io. 📊 Dashboard - Accuracy and efficiency scoring trends for contained plugins (

Get the whole plugin

Other skills on dotnet-skills.