Accepting uploads
Files reach an API in one of two ways: as the whole body of a PUT request, which suits scripts and apps, or as a field
in an HTML form. This page covers both, how to let only one of several editors save, and how to accept files larger
than the web server allows by default.
Note
This endpoint leaves out authorization to stay short. Before you use it, see Securing file endpoints.
app.MapPut("/files/{container}/{**name}", (string container, string name, HttpRequest request,
IResourceServiceClient resources, CancellationToken cancellationToken) =>
resources.GetContainerClient(container).UploadFromRequestAsync(name, request, null, cancellationToken));
UploadFromRequestAsync streams the request body into storage, so nothing is buffered whatever its size. It stores
the request's Content-Type, and passes its Content-Length as the declared length, so a truncated body throws
ResourceContentLengthMismatchException instead of storing part of a file. Like any PUT, it replaces the current
version, whether or not the container keeps versions. It answers:
- 201 Created when the name had no current version, and 200 OK when it replaced one, either way with the
stored
ResourceInfoas JSON, itsETag, and its URI asLocationwhen a URI provider gives one; - 412 Precondition Failed, with the current version's
ETag, when the request's preconditions don't hold.
The preconditions are decided by the write itself, so of clients that read the same version and upload changes, exactly one succeeds (see Updating only if unchanged):
| Request header | Stores the upload only if |
|---|---|
If-Match: "<ETag from a download>" |
the current version still has that content |
If-Match: * |
the name has a current version |
If-None-Match: * |
the name has no current version |
If-None-Match: "<ETag>" |
the current version isn't that one |
If-Unmodified-Since |
the current version hasn't changed since then |
If-Match compares tags strongly, so a weak tag (W/"…") never matches. Pass ResourceUploadOptions for metadata,
other headers to store, or Conditions; when the request has preconditions, they replace the options' Conditions.
The options' Mode 4.0 is ignored: a PUT replaces, unless the client sends If-None-Match: *. An endpoint
that must only create files can require that header, answering 428 Precondition Required without it.
With SQL Server 4.0, a transient failure partway through an upload is retried, even though a request body can't be read twice: the content goes in about a MiB at a time, and a batch that fails is sent again from what it holds.
From an HTML form
Take an IFormFile. ASP.NET Core buffers form files, to disk above 64 KB, before your endpoint runs:
builder.Services.AddAntiforgery();
// ...
app.UseAntiforgery();
app.MapPost("/files/{container}", async (string container, IFormFile file, IResourceServiceClient resources,
CancellationToken cancellationToken) =>
{
// A name your application chooses: the browser's file name is whatever the uploader says.
string name = $"uploads/{Guid.NewGuid():N}{Path.GetExtension(file.FileName)}";
await using Stream content = file.OpenReadStream();
ResourceInfo stored = await resources.GetContainerClient(container).UploadAsync(name, content,
new ResourceUploadOptions
{
Headers = new ResourceHeaders(file.ContentType, file.Length),
Metadata = new Dictionary<string, string> { ["fileName"] = file.FileName }
}, cancellationToken);
return Results.Created(stored.Uri, stored);
});
Minimal APIs check the form's antiforgery token for every endpoint that takes a form file, once UseAntiforgery is
in the pipeline, so a page on another site can't make a signed-in user's browser upload for it. Render the token in
your form with @Html.AntiForgeryToken() or <AntiforgeryToken />. Keep the browser's file name as metadata if you
want to show it, but don't use it as the resource name: it can be anything, including a name that overwrites another
file.
A form file's stream can seek, so with SQL Server a transient failure partway through retries the upload.
Large uploads
Kestrel rejects request bodies over about 30 MB with 413 Payload Too Large, forms included, and form files are also limited to 128 MB. Raise the limits for the endpoints that take large files:
var fiveGigabytes = new RequestSizeLimitAttribute(5L * 1024 * 1024 * 1024);
app.MapPut("/files/{container}/{**name}", UploadAsync).WithMetadata(fiveGigabytes);
app.MapPost("/files/{container}", UploadFormAsync).WithMetadata(fiveGigabytes).DisableAntiforgery();
builder.Services.Configure<FormOptions>(options => options.MultipartBodyLengthLimit = 5L * 1024 * 1024 * 1024);
Behind IIS or a reverse proxy, raise its limit too (maxAllowedContentLength, client_max_body_size). For files
large enough that a dropped connection is likely, or behind a limit you can't raise, use
resumable uploads instead: the client sends the file in chunks, and resumes where it stopped.