MVC, Razor Pages and Blazor
The rest of this section uses minimal APIs, but the same pieces work in controllers, Razor Pages and Blazor, with a few differences this page covers. Everything here leaves out authorization to stay short; see Securing file endpoints.
MVC controllers
Controller actions can return the same results as minimal APIs, Results.Resource and UploadFromRequestAsync's,
because MVC runs any IResult an action returns. The Results.Resource(container, name) form, which answers range
requests, is new in 4.0 4.0; with 3.1, download first and pass the
download, as in Serving downloads:
using Microsoft.AspNetCore.Mvc;
using Resourcefull;
using Resourcefull.AspNetCore;
[ApiController]
[Route("files")]
public class FilesController(IResourceServiceClient resources) : ControllerBase
{
// Downloads, with ETags, conditional requests and ranges.
[HttpGet("{**name}")]
public IResult Get(string name) => Results.Resource(resources.GetContainerClient("files"), name);
// Uploads the request body as it arrives, of any size up to the limit below.
[HttpPut("{**name}")]
[RequestSizeLimit(1L * 1024 * 1024 * 1024)]
public Task<IResult> Put(string name, CancellationToken cancellationToken) =>
resources.GetContainerClient("files").UploadFromRequestAsync(name, Request, null, cancellationToken);
}
For a form upload in an MVC view, take an IFormFile and keep [ValidateAntiForgeryToken] on the action, as
Accepting uploads explains for minimal APIs.
Razor Pages
A Razor page's handler can't return an IResult: ASP.NET Core refuses it at run time, with "Unsupported handler method
return type". From 4.0 4.0, the results TypedResults.Resource, TypedResults.SharedResource and TypedResults.SharedUpload return are
action results too, so a handler can return them. The download results work as File(...) does, conditional requests
included, and ranges when the result looks the resource up, as here:
public IActionResult OnGet(string name) =>
TypedResults.Resource(_resources.GetContainerClient("files"), name);
Use TypedResults, not Results: Results.Resource is declared to return IResult, which a handler can't return.
With 3.1, serve files from a minimal API endpoint next to your pages, as in Serving downloads, and link to it from the page, or run the result yourself in the handler and return an empty result, so the page doesn't render after it:
public async Task<IActionResult> OnGetAsync(string name, CancellationToken cancellationToken)
{
ResourceDownloadResult? download = await _resources.GetContainerClient("files")
.DownloadStreamingAsync(name, cancellationToken: cancellationToken);
if (download is null) return NotFound();
await Results.Resource(download).ExecuteAsync(HttpContext);
return new EmptyResult();
}
Blazor
Where your components run decides how they reach your files.
Blazor Server
Components with interactive server rendering run on the server, so they can inject IResourceServiceClient and use it
as any other code does. (A statically rendered component has no InputFile events; give it a form that posts to an
upload endpoint instead.)
Uploads: take the file from an
InputFile, and upload the stream it gives you.OpenReadStreamrefuses files over 500 KB unless you pass a larger limit, and the content travels over the page's connection, so for large files an upload endpoint the browser sends to directly is faster.private async Task UploadAsync(InputFileChangeEventArgs e) { await using Stream content = e.File.OpenReadStream(maxAllowedSize: 100 * 1024 * 1024); // Browsers report an empty type for files they don't recognize; store none rather than an empty one. string? type = string.IsNullOrEmpty(e.File.ContentType) ? null : e.File.ContentType; await Resources.GetContainerClient("files").UploadAsync(e.File.Name, content, new ResourceUploadOptions { Headers = new ResourceHeaders { ContentType = type, ContentLength = e.File.Size } }); }Downloads: don't read a file into memory and send it through the page's connection to the browser. Link to a download endpoint instead, or to a shared link, so the file streams straight from storage to the browser. Escape each part of the name between
/s, not the whole name: an escaped/doesn't match the endpoint's{**name}.<a href="@($"files/{string.Join('/', name.Split('/').Select(Uri.EscapeDataString))}")">Download</a>
The Blazor sample works this way.
Blazor WebAssembly
Components run in the browser, which can't reach your database, so they go through your API: the
endpoints in this section, called with HttpClient.
await using Stream content = file.OpenReadStream(maxAllowedSize: 100 * 1024 * 1024);
using var body = new StreamContent(content);
if (!string.IsNullOrEmpty(file.ContentType)) body.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
HttpResponseMessage response = await Http.PutAsync($"files/{Uri.EscapeDataString(file.Name)}", body);
In the browser, HttpClient reads the whole body into memory before it sends it, so size maxAllowedSize for what
the browser can hold. Browsers that support it can stream instead: build an HttpRequestMessage and call
request.SetBrowserRequestStreamingEnabled(true), which needs HTTP/2 (HTTPS) and a Chromium-based browser.
Download with a link to the endpoint, as for Blazor Server. Components with interactive auto rendering may run in either place, so treat them as WebAssembly components.