File downloads are common in web applications, from report exports to document delivery and media processing. The implementation must consider more than returning a file: authorization, memory usage, content types, caching, and interrupted connections all matter.
For large files, streaming can help avoid loading the entire response into application memory.
Avoid Loading Large Files Entirely into Memory
A simple endpoint might read a complete file into a byte array before returning it.
That approach can be reasonable for small files, but it becomes expensive when many users request large downloads simultaneously.
Streaming allows data to be transferred progressively rather than requiring the application to hold the entire file in memory.
Return a File from ASP.NET Core
ASP.NET Core provides file-result APIs for returning files and streams.
app.MapGet("/api/reports/{id}/download", async (
int id,
ReportService reports,
CancellationToken cancellationToken) =>
{
var report = await reports.OpenReportAsync(
id,
cancellationToken);
if (report is null)
{
return Results.NotFound();
}
return Results.File(
report.Stream,
contentType: "application/pdf",
fileDownloadName: report.FileName);
});
This is an illustrative example. ReportService and its result type must be defined by the application, and the stream’s ownership and disposal behavior must be clear.
The endpoint must also authorize the caller before returning the file. Knowing a report ID must not automatically grant access.
Set Accurate Content Types and File Names
Use a content type that matches the actual file format. Do not trust an arbitrary client-supplied content type when serving uploaded content.
Download filenames should be controlled and validated so they cannot introduce unsafe path information or misleading file extensions.
If files are user-generated, consider whether they should be served from a separate origin or with download-oriented content-disposition behavior to reduce browser security risks.
Handle Stream Lifetimes Correctly
A stream must remain open for as long as the response needs it. Disposing it too early can interrupt the download, while failing to dispose it can leak file handles or other resources.
Make ownership explicit in the service and endpoint design. For streams backed by remote storage, also consider how cancellation and connection failures are handled.
Support Large or Interrupted Downloads Deliberately
Some applications need range requests so clients can resume downloads or retrieve only part of a file. Verify the capabilities of the selected ASP.NET Core result type and storage mechanism rather than assuming every stream supports range processing.
For very large objects, redirecting to a short-lived, authorized object-storage URL may reduce the workload on the application server. Such URLs should be scoped and expire according to the application’s security requirements.
Prevent Unnecessary File Access
Before opening a file, validate that the authenticated user can access the corresponding record. Prefer identifiers mapped to authorized resources rather than accepting arbitrary filesystem paths from requests.
Do not concatenate user input into filesystem paths without robust validation and containment checks.
Log download failures and relevant metadata, but avoid recording sensitive file contents or credentials.
Conclusion
Reliable ASP.NET Core downloads require correct authorization, streaming-aware resource management, accurate content metadata, and deliberate handling of large or interrupted transfers.
Choose the response mechanism based on file size, storage location, and client requirements, and ensure that every download respects the application’s access-control rules.