FileUpload
A static page with IzzyBlazor's script. No circuit.
Usage
<Form Model="Model" FormName="profile" Multipart OnValidSubmit="Save">
<FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg"
MaxFileSize="2_000_000" />
<Button Submit>Save</Button>
</Form>
{
// The file binds by its Name; check its size and type in Save.
[SupplyParameterFromForm(FormName = "profile")]
private IFormFile? Avatar { get; set; }
}The zone is a real file input with a label that opens the picker, so it works on static pages with JavaScript off: the file posts with the form, and the page binds it by its Name. Set Multipart on the Form, or only the file's name posts.
Types, size and posting
Use Accept for the types offered and MaxFileSize for the largest file, in bytes; together they write the line under the prompt ("PNG or JPEG · up to 2 MB").
Preview
Code
Microsoft.AspNetCore.Http
<Form Model="Model" FormName="file-upload-photo" Multipart OnValidSubmit="Save" MaxWidth="26.Rem" Grow>
<FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg" MaxFileSize="MaxSize"
Hint="Square images work best." OnChange="Chosen" />
<Row><Button Submit StartIcon="Lucide.Upload">Upload</Button></Row>
if (_result is not null)
{
<Alert Status="_status">_result</Alert>
}
</Form>
{
private const long MaxSize = 2_000_000;
[SupplyParameterFromForm(FormName = "file-upload-photo")]
private Upload? Model { get; set; }
// Static pages: the file posts with the form, bound by its Name.
[SupplyParameterFromForm(FormName = "file-upload-photo")]
private IFormFile? Avatar { get; set; }
// Interactive pages: OnChange gets the file as it's chosen.
private IBrowserFile? _chosen;
private string? _result;
private Status _status;
protected override void OnInitialized() => Model ??= new();
private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files.FirstOrDefault();
// Accept and MaxFileSize only guide the browser, and the name and type come from it too:
// check the size, then the file's first bytes.
private async Task Save()
{
var (name, size) = Avatar is { Length: > 0 } posted ? (posted.FileName, posted.Length)
: _chosen is { } chosen ? (chosen.Name, chosen.Size)
: (null, 0L);
if (name is null) (_status, _result) = (Status.Warning, "Choose a photo first.");
else if (size > MaxSize) (_status, _result) = (Status.Danger, $"{name} is larger than 2 MB.");
else
{
await using var stream = Avatar is { Length: > 0 } ? Avatar.OpenReadStream() : _chosen!.OpenReadStream(MaxSize);
var head = new byte[4];
var read = await stream.ReadAtLeastAsync(head, head.Length, throwOnEndOfStream: false);
(_status, _result) = IsPngOrJpeg(head.AsSpan(0, read))
? (Status.Success, $"Uploaded {name} ({size:N0} bytes).")
: (Status.Danger, $"{name} isn't a PNG or JPEG.");
}
}
// PNG files start with 89 50 4E 47, JPEG files with FF D8 FF.
private static bool IsPngOrJpeg(ReadOnlySpan<byte> head) =>
head.StartsWith((byte[])[0x89, 0x50, 0x4E, 0x47]) || head.StartsWith((byte[])[0xFF, 0xD8, 0xFF]);
// The form's model; the file itself binds to Avatar above.
public sealed class Upload;
}Static: You can also drop a photo on the zone; one over 2 MB is flagged before posting, and the chosen file's name and size replace the types and size line.
On a static page, bind the file to an IFormFile property with [SupplyParameterFromForm], named like the field. Both limits only guide the browser: the picker's "All files" choice gets past Accept, and with JavaScript off nothing checks the size, so check both on the server, as the example does by size and by the file's first bytes.
Several files
Use Multiple to take several files at once, and bind them to an IReadOnlyList<IFormFile> (or an IFormFileCollection) on a static page.
Preview
Code
Microsoft.AspNetCore.Http
<Form Model="Model" FormName="file-upload-multiple" Multipart OnValidSubmit="Send" MaxWidth="26.Rem" Grow>
<FileUpload Name="Attachments" Label="Attachments" Multiple Accept=".pdf,.docx" MaxFileSize="MaxSize"
Hint="Contracts and statements of work." OnChange="Chosen" />
<Row><Button Submit StartIcon="Lucide.Paperclip">Attach</Button></Row>
if (_result is not null)
{
<Alert Status="_status">_result</Alert>
}
</Form>
{
private const long MaxSize = 10_000_000;
[SupplyParameterFromForm(FormName = "file-upload-multiple")]
private Message? Model { get; set; }
// Static pages: every chosen file posts under the same name.
[SupplyParameterFromForm(FormName = "file-upload-multiple")]
private IReadOnlyList<IFormFile>? Attachments { get; set; }
// Interactive pages: OnChange gets the files as they're chosen.
private IReadOnlyList<IBrowserFile> _chosen = [];
private string? _result;
private Status _status;
protected override void OnInitialized() => Model ??= new();
private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files;
private void Send()
{
var files = Attachments is { Count: > 0 } posted
? posted.Where(f => f.Length > 0).Select(f => (f.FileName, f.Length)).ToList()
: _chosen.Select(f => (FileName: f.Name, Length: f.Size)).ToList();
// The picker's "All files" and a crafted post get past Accept: check here too.
var wrong = files.FirstOrDefault(f => !f.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
&& !f.FileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase));
(_status, _result) = files.Count == 0 ? (Status.Warning, "Choose a file first.")
: wrong.FileName is { } name ? (Status.Danger, $"{name} isn't a PDF or Word file.")
: files.Any(f => f.Length > MaxSize) ? (Status.Danger, "Each file must be 10 MB or less.")
: (Status.Success, $"Attached {string.Join(", ", files.Select(f => f.FileName))}.");
}
// The form's model; the files themselves bind to Attachments above.
public sealed class Message;
}Static: Files can be dropped on the zone too (other types are refused), and "2 files" replaces the types and size line.
Every chosen file posts under the same name. A file dropped on the zone, or chosen again, replaces the earlier choice rather than adding to it, and the zone doesn't list the files, so show what arrived after the post.
Required and disabled
Use Required for a file the form can't post without, and Disabled to turn the zone off.
Preview
Code
Microsoft.AspNetCore.Http
<Form Model="Model" FormName="file-upload-states" Multipart OnValidSubmit="Apply" MaxWidth="26.Rem" Grow>
<FileUpload Name="Resume" Label="Résumé" Required Accept=".pdf" MaxFileSize="5_000_000" OnChange="Chosen" />
<FileUpload Name="Logo" Label="Company logo" Accept="image/svg+xml" Disabled
Hint="Disabled: only the workspace owner can change it." />
<Row><Button Submit>Apply</Button></Row>
if (_result is not null)
{
<Alert Status="_status">_result</Alert>
}
</Form>
{
[SupplyParameterFromForm(FormName = "file-upload-states")]
private Application? Model { get; set; }
[SupplyParameterFromForm(FormName = "file-upload-states")]
private IFormFile? Resume { get; set; }
private IBrowserFile? _chosen;
private string? _result;
private Status _status;
protected override void OnInitialized() => Model ??= new();
private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files.FirstOrDefault();
// Required and Accept are the browser's checks: a crafted post can leave the file out or send
// another type. A disabled field posts nothing, so Logo never arrives.
private void Apply() => (_status, _result) = (Resume is { Length: > 0 } ? Resume.FileName : _chosen?.Name) switch
{
null => (Status.Warning, "Choose a résumé first."),
var name when !name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) => (Status.Danger, $"{name} isn't a PDF."),
var name => (Status.Success, $"Sent {name}."),
};
public sealed class Application;
}Required is the browser's check, so a crafted post can still leave the file out: check that it arrived on the server too. A file can't be read-only, so a read-only Form disables the zone. A disabled zone posts nothing, so keep its file on the server rather than clearing it after the post.
Checking on the server
Check every file's size and type where you save it, and set the server's request limits to match MaxFileSize: Kestrel turns away a request over 30,000,000 bytes (about 28.6 MB) by default, before your code runs.
using Microsoft.AspNetCore.Http.Features;
// Kestrel's limit for the whole request (30,000,000 bytes by default).
builder.WebHost.ConfigureKestrel(kestrel =>
kestrel.Limits.MaxRequestBodySize = 12_000_000);
// The limit for a form's files (128 MB by default).
builder.Services.Configure<FormOptions>(form =>
form.MultipartBodyLengthLimit = 12_000_000);The posted content type and file name come from the browser and can say anything, so don't trust either to name the saved file or to decide how it's served; check the file's first bytes for its type, as the photo example does. IIS has its own request limit (about 28.6 MB by default) when the app runs behind it.
On an interactive page
Use OnChange to get the chosen files as IBrowserFile as soon as they're chosen, and read each with OpenReadStream; nothing posts.
<FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg"
MaxFileSize="MaxSize" OnChange="UploadAsync" />
@* PhotoStore is your own service. *@
PhotoStore Photos
{
private const long MaxSize = 2_000_000;
private async Task UploadAsync(IReadOnlyList<IBrowserFile> files)
{
if (files is not [var file]) return;
await using var stream = file.OpenReadStream(MaxSize);
// Check the file's first bytes for its type before keeping it.
await Photos.SaveAsync(stream);
}
}OnChange leaves out files over MaxFileSize and gives at most MaxFiles (10). OpenReadStream throws for a file over 512,000 bytes unless you pass a larger limit, so pass MaxFileSize.
Its own words
Use ChooseText and DragText for the prompt, AcceptText for the line under it (empty for none), and TooLargeText and WrongTypeText for the messages, or set them for the whole app as in Text and other languages.
Mode support
| Feature | Off | Static | Live |
|---|---|---|---|
| Choose files with the picker, posted with the form | Yes | Yes | Yes |
| Drop files on the zone; the wrong type refused | No | Yes | Yes |
| The chosen file's name and size, too large flagged before posting | No | Yes | Yes |
| OnChange gets the files | No | No | Yes |
Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).
Accessibility
- It renders a real file
input, out of sight but focusable once scripts run: Tab reaches it, the zone shows the focus ring, and Enter or Space opens the picker. - The input is named by
Label; without one, by the "Choose a file" link ("Choose files" withMultiple;ChooseText). - The allowed types and size, the
Hintand anyErrorsare linked witharia-describedby;Errorsalso setsaria-invalid. - With script, the chosen file's name and size (or "3 files") are announced politely; there's no list of the files.
- With script, a file of the wrong type or over
MaxFileSizegets the browser's validation message, and the browser's own validation keeps the form from posting (not withNativeValidation="false"on the Form). - Dropping is a pointer-only extra: the picker does the same from the keyboard.
Parameters
| Prop | Type | Default |
|---|---|---|
NameThe form field name the files post under. | string | Required |
LabelThe visible label. | string? | |
HideLabelHides the label but keeps it for screen readers. | bool | false |
HintHelp text under the zone. | string? | |
RequiredWhether a file must be chosen before the form posts. | bool | false |
DisabledDisables the picker. | bool | false |
AcceptThe file types to offer, as the accept attribute: "image/*", ".pdf,.docx". | string? | |
MultipleAllows choosing several files. | bool | false |
MaxFileSizeThe largest file allowed, in bytes. | long? | |
MaxFilesOn interactive pages, the most files OnChange receives; extra ones are left out. | int | 10 |
TooLargeTextThe message for a file over MaxFileSize: {0} is its name, {1} the limit. | string? | |
WrongTypeTextThe message for a chosen or dropped file that doesn't match Accept: {0} is its name. | string? | |
ChooseTextThe link that opens the picker. | string? | computed |
DragTextThe text after the link. | string? | computed |
AcceptTextSmall text under the prompt. | string? | computed |
IconThe zone's icon. | IconData? | computed |
ErrorsValidation messages to show under the zone, such as from server checks. | IReadOnlyList<string>? | |
OnChangeOn interactive pages, runs with the chosen files, leaving out any over MaxFileSize (the zone flags those). | EventCallback<IReadOnlyList<IBrowserFile>> |
Plus 16 item props and 4 common props.
Full APISee also
- Form: posts on static pages, validates live on interactive ones.
- TextField: a labeled text input with add-ons and a password reveal.
- FieldFrame: the label, hint and errors around a control of your own.