Updated on
Returning a file from an ASP.NET Core Web API is one method call: File(), declared on ControllerBase, which takes the bytes, the content type, and the name we want the browser to save the download as.
What changes between one download and the next is the first argument. A byte[] gives us a FileContentResult, a Stream gives us a FileStreamResult, a path relative to the web root gives us a VirtualFileResult, and the sibling method PhysicalFile() takes an absolute path on disk and gives us a PhysicalFileResult.
Let’s start it.
VIDEO: How to Return a File From a Web API.
What Is a MIME Type?
A MIME type is the label that tells a client what kind of bytes it just received. File extensions mean nothing over HTTP, so the Content-Type header carries the format instead.
The value has two parts separated by a slash, a type and a subtype. image/png is an image whose subtype is PNG, application/pdf is a PDF, and text/csv is comma-separated text.
Getting it wrong shows up in the client. A PDF sent as text/plain with nosniff set renders as garbled characters instead of opening in the viewer, and an image sent as application/octet-stream downloads when it could have displayed inline.
Sending application/octet-stream on purpose is still right when we genuinely do not know what the bytes are, because it tells the browser to treat them as an unknown binary file and save them.
When the content type depends on a file name we only learn at runtime, FileExtensionContentTypeProvider maps an extension to a content type, so we do not have to keep our own lookup table.
It is good to mention that, application/octet-stream is a default MIME Type in case we need to return an unknown file.
To learn more about the most common MIME Types, check the Common media types article.
Should We Return a Byte Array or a Stream?
Both work, and the difference is memory. A byte[] holds the entire file in managed memory before the response starts. For the logo in this example that is a few kilobytes, which does not matter. For a 200 MB export it is 200 MB per concurrent request, and anything from 85,000 bytes up lands on the large object heap, which is expensive to collect.
A Stream hands the framework an open pipe and lets it copy the file to the response one buffer at a time. Peak memory then tracks the buffer size, not the file size.
Reach for the byte array when the file is small, fixed and already in memory. Reach for the stream for anything a user could make large, and anything read off disk, a blob store or another HTTP response.
One rule attaches to the stream overloads: ASP.NET Core disposes the stream once the response has been sent. We hand it over and do not close it ourselves.
The difference is easiest to see side by side:

You can check the Convert Byte Array to File in C# article to learn more about byte arrays and the C# Back to Basics: Files, StreamWriter and StreamReader to learn more about Stream. Our sample service hands back a MemoryStream holding the whole logo, and the upload side of the same trade-off is covered in our article on streaming large files instead of buffering them.
Web API Controller Preparation
Once we have an ASP.NET Core Web API ready, let’s create an empty API Controller:
[Route("api/[controller]")]
[ApiController]
public class DownloadsController(IFileService fileService) : ControllerBase
{
private readonly IFileService _fileService = fileService;
private const string MimeType = "image/png";
private const string FileName = "CM-Logo.png";
}
Note that we have a _fileService, a MimeType, and a FileName field. The _fileService contains the logic to transform the file into a byte array or a stream. For this article, we are not going into detail about how it works, but the FileService class in the source code does it with Convert.FromBase64String().
The MimeType field represents the nature of the file we want to return and the FileName represents a suggestion file name to download.
How Do We Return a File as a Byte Array?
Now, let’s create our first ReturnByteArray action to return a file using Byte Array:
[HttpGet("images-byte")]
public IActionResult ReturnByteArray()
{
var image = _fileService.GetImageAsByteArray();
return File(image, MimeType, FileName);
}
Note that this is an images-byte route with the Get HTTP Verb. Since we are using the same file result for every request, we don’t need any input parameter.
So, first, we use GetImageAsByteArray() from the FileService class to retrieve the image we want to return as an array of bytes.
Then, we return a call to the File method passing the image file as a byte array, the MimeType we set as a class field, and the file name we want to return, as parameters.
The File method lives under the ControllerBase abstract class and returns a FileContentResult, which is responsible to provide the file to download.
How Do We Return a File as a Stream?
Now, let’s create a ReturnStream action:
[HttpGet("images-stream")]
public IActionResult ReturnStream()
{
var image = _fileService.GetImageAsStream();
return File(image, MimeType, FileName);
}
We are going to keep the Get HTTP Verb, but this time, we set our route to images-stream.
This action is similar to the one in which we are using byte[] with the exception that, now, we are calling the GetImageAsStream() method from the FileService class.
Finally, we return a call to the File method. Note that this time we are passing a Stream object instead of a byte[] as the first parameter. This is possible because the File method has many overloads.
The stream is not ours to dispose. Microsoft’s ControllerBase.File() reference states it for every Stream overload: “The fileStream parameter is disposed after the response is sent.”
What Are the ControllerBase.File() Overloads?
ControllerBase declares twenty-four overloads of File(), and the first argument decides which result type comes back.
Pass a byte[] and we get a FileContentResult. Pass a Stream and we get a FileStreamResult. Pass a string virtual path, resolved under the web root, and we get a VirtualFileResult. The sibling method PhysicalFile() covers the fourth case: it takes an absolute path on disk and returns a PhysicalFileResult.
Past the first argument, all four families take the same optional extras. fileDownloadName sets the Content-Disposition header, which is what turns an inline render into a save dialog. lastModified and entityTag populate Last-Modified and ETag. enableRangeProcessing lets a client ask for part of the file and get 206 Partial Content back, which is how a video scrubs and how a paused download resumes.
Choosing an overload comes down to two questions: where the bytes are, and which of those headers we want set.
| We have | Call | We get back | Notes |
|---|---|---|---|
A byte[] in memory | File(byte[] fileContents, string contentType, string fileDownloadName) | FileContentResult | The whole file is already in managed memory before the response starts |
An open Stream | File(Stream fileStream, string contentType, string fileDownloadName) | FileStreamResult | ASP.NET Core disposes the stream after the response is sent |
| A path under the web root | File(string virtualPath, string contentType, string fileDownloadName) | VirtualFileResult | The path is resolved by the web root file provider, not by System.IO |
| An absolute path on disk | PhysicalFile(string physicalPath, string contentType, string fileDownloadName) | PhysicalFileResult | The documentation is explicit: "The path must be an absolute path" |
On ControllerBase none of the extra arguments is an optional parameter. Each combination is its own overload, so “left out” in the next table means calling an overload that does not take the argument:
| Argument | Type | When left out | What it does |
|---|---|---|---|
fileDownloadName | string | not set | Sets the Content-Disposition header, which is what turns an inline render into a save dialog |
lastModified | DateTimeOffset? | not set (a path overload sends the file's own date) | Populates Last-Modified, so a conditional request can be answered with 304 Not Modified |
entityTag | EntityTagHeaderValue | not set | Populates ETag, used for the same conditional-request handling |
enableRangeProcessing | bool | false | Lets the client ask for byte ranges and get 206 Partial Content, or 416 Range Not Satisfiable |
Range processing adds 206 and 416 to the status codes we get back, so let’s add an action that turns it on:
[HttpGet("images-stream-range")]
public IActionResult ReturnStreamWithRanges()
{
var image = _fileService.GetImageAsStream();
return File(image, MimeType, FileName, enableRangeProcessing: true);
}
With the flag on, the response carries Accept-Ranges: bytes. A request with Range: bytes=0-99 gets 206 Partial Content and Content-Range: bytes 0-99/3668, and a range past the end of our 3,668-byte logo gets 416 Range Not Satisfiable. The same Range header sent to images-stream returns a plain 200 with the whole file.
How Do We Return a File From a Minimal API?
Minimal API endpoints do not inherit from ControllerBase, so File() is not in scope there. The equivalent is Results.File(), or TypedResults.File() when we want the concrete result type in the signature for testing.
Results.File() has three overloads, and they line up with the controller ones. The byte[] overload is documented as an alias for Results.Bytes(), the Stream overload as an alias for Results.Stream(), and the string overload writes the file at a given path, resolving a relative path against the web root file provider rather than the current directory.
The optional arguments keep their names, so contentType, fileDownloadName, lastModified, entityTag and enableRangeProcessing all mean here what they mean on the controller. On these overloads they are genuine optional parameters, and enableRangeProcessing defaults to false.
The disposal rule carries over too: the endpoint hands the stream across, and the framework disposes it after the response is sent.
If you have not built one yet, our article on how minimal APIs are wired up covers the setup. In our sample, one statement in Program.cs returns the same image:
app.MapGet("/minimal/images-stream", (IFileService fileService) =>
Results.File(fileService.GetImageAsStream(), "image/png", "CM-Logo.png"));
The endpoint answers with the same Content-Type and Content-Disposition headers as the controller actions, and IFileService resolves from the container without any extra registration.
How Do Clients Handle the Downloaded File?
Now that our code is ready, let’s check the behavior when we use Postman and the browser.
Checking the Result Using Postman
Let’s check the behavior when we make requests via Postman:
As we can see, Postman doesn’t provide us a link or open the dialog box to download the image. Instead, it shows the image in its body response.
Browser
When we call any route (images-byte or images-stream) the browser downloads the image into the default download folder or opens the dialog box to indicate which folder we want to save the file.
Conclusion
In this article, besides learning some concepts behind how the web handles files, we have seen how to return a file using ASP.NET Core Web API. Also, we have checked how Postman and the browser behave when we request a file in a Web API. If the client is an Angular application, the next step is downloading the file from an Angular client.
Tested with .NET 10.0.10 and Microsoft.AspNetCore.OpenApi 10.0.12.


what about memory leak ?
[HttpGet("images-stream")] public IActionResult ReturnStream() { var image = _fileService.GetImageAsStream(); return File(image, MimeType, FileName); }Hi, this is the official statement from Microsoft:
Of course, this is valid if your comment was related to this.
Any links highly appreciated …
Hi. If you think about that quote, this is the link:https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream?view=net-7.0#remarks
IFileService returns Stream not MemoryStream. It is very prone to errors when you just change the implementation. I think one extra word “using” does not make the example hard to understand.
Yes, but the implementation of the method works with MemoryStream. But ok, adding the using keyword won’t hurt anyone.
so FileStreamResultExecutor will make it but it is worth to mention that since it is not obvious.
Where’s this magical File() coming from?
Hi James. I am not sure why is it magical, but if you inspect our source code, which you can find at the top of each of our articles, you will find the method inside the controller: https://github.com/CodeMazeBlog/CodeMazeGuides/blob/main/aspnetcore-webapi/ReturnFileWebApi/ReturnFileWebApi/Controllers/DownloadsController.cs
As you can return NotFound, BadRequest, or OK from an action inside the controller, you can return File as well.
It’s magical because it only belongs to MVC not WebAPI controllers and it’s a function inherited not imported.
Well, James, you asked where this method is coming from and I just assumed that you don’t know (just by reading the article) how we use this method and how we access it. So, I gave you the simple answer. Now, if you go that deep into the Microsoft.AspNetCore.Mvc, you will find the ControllerBase class, which contains all the methods that I mentioned in my reply. Since each controller inherits that ControllerBase class, this is also part of the Web API project.
But at this point, I am not sure what was the point of your initial question.
Right. Sorry if I seemed frustrated; I was not; I said that with a smile. Everything you said is correct. This just does not apply to WebAPI users. You might make that clear to the readers, but I seem to be the only stupid one here. In another coincidence, there was an error in a middleware that we have which I did not write, but I see that someone else had the same error, so I assume both came from the same code sample that was copied. When I implemented the WebAPI version, the error presented itself, and I didn’t know which way was up or down. Answers to my question with hundreds of upvotes didn’t work for me until I fixed the issue with the middleware.
I couldn’t find the link to
“How to consume this API in client application”
Please help
I am not sure that I understand. What link are you referring to?
How retrieve image as part of json endpoint in webapi 2 asp.net?
Thanks for sharing.
see how to consume this service from angular.
From this moment I thank you for your answer.
You can read more about it here: https://code-maze.com/download-files-dot-net-core-angular/