IFormFile is convenient, but it uses buffered model binding. ASP.NET Core keeps smaller files in memory and moves files above the default 64 KiB FormOptions.MemoryBufferThreshold to temporary disk before the action processes them.

That can be reasonable for bounded, infrequent uploads. Large files or many concurrent uploads can exhaust memory, temporary storage, or disk I/O before your endpoint starts its own work.

Streaming is not automatically faster. Its main benefit is avoiding the memory and temporary-disk cost of buffering the complete upload.

Use MultipartReader when the workload needs incremental processing without buffering the complete file through IFormFile. After validating the multipart content type and extracting its boundary within FormOptions.MultipartBoundaryLengthLimit, configure the reader limits explicitly:

// Inside a Minimal API handler, ASP.NET Core 10+
var reader = new MultipartReader(boundary, request.Body)
{
    BodyLengthLimit = limits.MaxFileSize,
    HeadersCountLimit = limits.MaxSectionHeaders,
    HeadersLengthLimit = limits.MaxSectionHeaderBytes
};

await using var stagedUpload =
    await stagingStore.BeginAsync(cancellationToken);

var fileCount = 0;

while (await reader.ReadNextSectionAsync(cancellationToken)
       is { } section)
{
    var fileSection = section.AsFileSection();

    if (fileSection is null ||
        !string.Equals(
            fileSection.Name,
            "file",
            StringComparison.Ordinal))
    {
        return Results.BadRequest(
            "The multipart request contains an unexpected section.");
    }

    if (++fileCount > 1)
    {
        return Results.BadRequest("Only one file is allowed.");
    }

    await stagedUpload.CopyFromAsync(
        section.Body,
        cancellationToken);
}

if (fileCount == 0)
{
    return Results.BadRequest("The request does not contain a file.");
}

await stagedUpload.CompleteAsync(cancellationToken);
return Results.NoContent();

Here, limits and stagingStore are application-specific abstractions. Convert each section with AsFileSection() because multipart requests can also contain normal form fields. This endpoint accepts exactly one file section named file and rejects every other shape.

The staging implementation must consume section.Body incrementally. Copying it into a byte array or MemoryStream would move the buffering into application code. If the request is rejected, a reader limit is hit, storage fails, or the request is cancelled before CompleteAsync, disposing the staged upload must discard the partial content. CompleteAsync finalizes the staged object; it does not promote the upload to trusted or publicly available storage. Validate the staged content before promotion; extension, declared and detected content types, malware scanning, and format-specific checks remain separate concerns.

BodyLengthLimit applies to each multipart section. HeadersCountLimit and HeadersLengthLimit bound the per-section headers. They do not replace the server or endpoint limit for the complete request body.

Multipart parsing and reader-limit failures can throw InvalidDataException. Map only expected parser failures to client responses: malformed multipart structure normally becomes 400 Bad Request, while a file section above the accepted maximum normally becomes 413 Content Too Large. Keep that mapping at the parsing boundary so an InvalidDataException from storage or later validation remains a server failure, and let staged-upload disposal remove any partial content.

For a Minimal API handler, do not bind IFormFile, IFormCollection, or other form-backed parameters, and do not access request.Form before reading request.Body. Controller-based endpoints need the same protection: disable form value model binding before the action reads the request body. Because this endpoint rejects every non-file section, browser clients using token-based antiforgery validation must send the token in the configured antiforgery header, not as a multipart form field.

Streaming avoids buffering the complete file through ASP.NET Core form model binding. It does not remove the need to bound request size, concurrent uploads, staging capacity, timeouts, or downstream writes. Keep cancellation connected through the reader and storage operation.

Choose streaming from the combined file size, concurrency, upload frequency, temporary-storage capacity, and downstream write behavior. It trades model-binding convenience for manual parsing, limit enforcement, validation, and cleanup.