All articles

Returning a File using FileResult in ASP.NET Core MVC

Return downloads from ASP.NET Core with FileResult — byte arrays, streams, and physical files, content types, inline vs attachment, range processing, and serving user uploads safely.

0 · log in to like, save & follow Share on LinkedIn Share on X

Whenever your ASP.NET Core application hands a browser something to download — a CSV export, a PDF invoice, an image, a generated report — the action returns a FileResult. This tutorial covers the three overload families (bytes, stream, file on disk), the headers they produce (verified with curl), content types, inline display vs. download, and range support for large files.

cover

The three ways to return a file

All three come from ControllerBase.File(...), so they work in MVC controllers and API controllers alike.

1. From a byte array — FileContentResult

For content you build in memory:

public FileResult DownloadBytes()
{
    var bytes = Encoding.UTF8.GetBytes("ProductId,Name\n1,Galaxy A15\n");
    return File(bytes, "text/csv", "products.csv");
}

Verified response:

200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename=products.csv

The third argument is what turns the response into a download — it sets Content-Disposition: attachment with the filename the user's browser will save. Omit it and the browser tries to display the content inline instead.

2. From a stream — FileStreamResult

For content that's already a stream — a MemoryStream you generated into, a cloud-storage download, anything you don't want fully buffered as byte[]:

public FileResult DownloadStream()
{
    var stream = new MemoryStream(Encoding.UTF8.GetBytes("streamed content"));
    return File(stream, "application/octet-stream", "data.bin");
}

The framework disposes the stream for you after writing the response. This is the overload behind real-world exports — the SQL-to-Excel tutorial saves a ClosedXML workbook into a MemoryStream and returns exactly this.

3. From disk — PhysicalFileResult / VirtualFileResult

// absolute path on the server
public IActionResult Download() =>
    PhysicalFile("/app/storage/report.pdf", "application/pdf", "report.pdf");

// path relative to wwwroot
public IActionResult Logo() =>
    File("~/images/logo.png", "image/png");

PhysicalFile takes an absolute path; the File(virtualPath, …) string overload resolves inside wwwroot. Never build these paths from user input without validation — File("~/" + fileName, …) with fileName = "../appsettings.json" is a path-traversal hole. Map user-supplied identifiers to server-side paths through a lookup, not string concatenation.

Content types

The second argument is the MIME type the browser receives. Common ones:

File Content type
PDF application/pdf
Excel (.xlsx) application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
CSV text/csv
PNG / JPEG image/png, image/jpeg
ZIP application/zip
Anything binary/unknown application/octet-stream

Don't guess from the extension by hand — FileExtensionContentTypeProvider maps extensions to MIME types if you serve arbitrary stored files.

Inline vs. attachment

  • File(bytes, "application/pdf")no filename → rendered inline (the PDF opens in the browser tab).
  • File(bytes, "application/pdf", "invoice.pdf")filename → downloaded as invoice.pdf.

That's the whole difference; pick per use case.

Large files: enable range processing

For big downloads (videos, backups), let clients resume and seek:

return PhysicalFile(path, "video/mp4", enableRangeProcessing: true);

The response then honors Range: headers with 206 Partial Content — required for media scrubbing and download managers.

Resolving content types for arbitrary files

Serving stored files whose types vary? Map extension → MIME type with the built-in provider instead of a hand-rolled switch:

private static readonly FileExtensionContentTypeProvider ContentTypes = new();

public IActionResult Stored(string name)
{
    var path = ResolveSafely(name);                     // your lookup, never raw user input
    if (!ContentTypes.TryGetContentType(path, out var contentType))
        contentType = "application/octet-stream";       // safe default: download, don't render
    return PhysicalFile(path, contentType);
}

application/octet-stream as the fallback matters — serving an unknown file as text/html would let an uploaded HTML file execute scripts in your origin.

Relaying a file from another service

When the file lives behind another HTTP API (cloud storage, an internal service), stream it through without buffering the whole thing in memory:

public async Task<IActionResult> Relay(string id, CancellationToken ct)
{
    var upstream = await httpClient.GetAsync(
        $"https://files.internal/{id}",
        HttpCompletionOption.ResponseHeadersRead, ct);   // headers now, body as it streams
    if (!upstream.IsSuccessStatusCode) return NotFound();

    var stream = await upstream.Content.ReadAsStreamAsync(ct);
    return File(stream, upstream.Content.Headers.ContentType?.ToString()
        ?? "application/octet-stream");
}

ResponseHeadersRead is the key — without it, GetAsync buffers the entire body before your code runs, which for a 2 GB file is a memory incident.

Testing actions that return files

FileResults are plain objects — assert on them directly:

[Fact]
public void Export_returns_csv_attachment()
{
    var result = controller.DownloadBytes();

    var file = Assert.IsType<FileContentResult>(result);
    Assert.Equal("text/csv", file.ContentType);
    Assert.Equal("products.csv", file.FileDownloadName);
    Assert.StartsWith("ProductId,Name", Encoding.UTF8.GetString(file.FileContents));
}

No server needed — the result type, content type, filename, and bytes are all inspectable.

Serving user uploads safely

The pattern from this platform's own image storage: keep uploads outside wwwroot, address them by an unguessable path, and stream them through an action:

[AllowAnonymous]
[HttpGet("/blog-media/{**path}")]
public async Task<IActionResult> Media(string path, CancellationToken ct)
{
    var stream = await storage.OpenReadAsync("blog-images", path, ct);
    return stream is null ? NotFound() : File(stream, ContentTypeFor(path));
}

You get access control, logging, and content-type control that static files can't give you — at the cost of a little throughput. For fully public, high-traffic assets, static files (or a CDN) win; for anything gated, FileResult is the tool.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.