Dynamically setting Content Type in ASP.NET Core with FileExtensionContentTypeProvider

tldr;

If you have a scenario where you have multiple file types (.pdf, .docx, etc.) stored somewhere (in a database, file system, etc.), that need to be downloaded, you can automatically figure out the Content Type by newing up a FileExtensionContentTypeProvider and call TryGetContentType to get the Content Type and pass that to the Fileresult helper.  See lines 8-16 below


[HttpPost("Download/{id:guid}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Download(Guid id)
{
// Retrieve some file from some service
ApplicationFile file = await _fileService.GetFileAsync(id);
var fileProvider = new FileExtensionContentTypeProvider();
// Figures out what the content type should be based on the file name.
if (!fileProvider.TryGetContentType(file.NameWithExtension, out string contentType))
{
throw new ArgumentOutOfRangeException($"Unable to find Content Type for file name {file.NameWithExtension}.");
}
// contentType will be application/pdf for .pdf's, application/vnd.openxmlformats-officedocument.presentationml.presentation for .pptx's, etc.
return File(file.Contents, contentType);
}

 

What is a Content Type?

A Content Type is how the server tells the browser what type of file the resource being served is.  That way the browser knows how to render whether it’s HTML, CSS, JSON, PDF, etc.  The way the server does this is by passing a Content-Type HTTP Header.  This value of the header will look like “text/html” for HTML, “text/css” for CSS, “application/json” for JSON, “application/pdf” for PDF’s, and so on.  A complete list can be found on the IANA official docs.

Note: A Content Type can also be called a MIME type, but because the header is called Content-Type, and ASP.NET Core calls it the Content Type in the code I’m going to be showing, I’m going to call it Content Type for consistency throughout this post.

 

How do I set the Content Type in ASP.NET Core?

The good news is, for a vast majority of the static files you’re going to serve, the Static Files Middleware will set the Content Type for you.  For scenarios where you need to set the Content Type yourself, you can use the FileContentResult in your Controllers or PageModels via the File helper method used on line 11 below.


[Route("File")]
public class FileController : Controller
{
[HttpPost("Download/{id:guid}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Download(Guid id)
{
// Grab a test.pdf back one directory. Look ma – it even runs on Linux with Path.DirectorySeparatorChar!
var fileContent = await File.ReadAllBytesAsync($"..{Path.DirectorySeparatorChar}test.pdf");
return File(fileContent, "application/pdf")
}
}

 

What happens if I set the wrong Content Type?

If you set the wrong Content Type, then you may cause issues for your application.  For example, the PDF rendered in the code above will render a PDF in the browser like this:

pdf returned

But what happens if I replace that “application/pdf” string with “application/json” to try to tell the browser the PDF is really JSON?  Well… let’s find out:

returned json

Well that’s not good.  So, setting the correct Content Type is pretty important.  (Also, yes I know I need to update Chrome…. don’t judge me.  I have a bunch of tabs open in another window that I’m totally going to look at some day, ok?)

 

Scenario

Let’s say you have a scenario where you allow admin users to upload files that allows some customer users to download those files.  Those admin users can uploads all sorts of file extensions such as a pdf, pptx, docx, xlsx, etc. that customers can then download themselves.  This means that you can’t assume and be sure what the Content Type should be, so you need to inspect the file extension to figure it out.  No big deal, there are a few ways to solve this, but the simplest is to just write a trusty ol’ switch statement like lines 11-25 below to handle every file type we allow.


[HttpPost("Download/{id:guid}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Download(Guid id)
{
// Retrieve some file from some service
ApplicationFile file = await _fileService.GetFileAsync(id);
string extension = Path.GetExtension(file.NameWithExtension);
string contentType = "";
switch (extension)
{
case ".pdf":
contentType = "application/pdf";
break;
case ".xslx":
contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
break;
case ".docx":
contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
break;
// and so on
default:
throw new ArgumentOutOfRangeException($"Unable to find Content Type for file name {file.NameWithExtension}.");
}
return File(file.Contents, contentType);
}

The problem with that is trying to maintain a list of all those mappings yourself is annoying, and likely leads you to adding “just one more” when your users want to support another file type you didn’t previously have.  It’s also prone to typo’s, because some of these content types are ridiculously convoluted.

 

Solution

Luckily, ASP.NET Core already has maintained this list for us via the FileExtensionContentTypeProvider.  So all you have to do is new it up, and call TryGetContentType which acts like a lot of the TryX-out pattern you see sprinkled throughout .NET.  It returns a bool and an out variable with the content type.  Usage looks like lines 8-16 below:


[HttpPost("Download/{id:guid}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Download(Guid id)
{
// Retrieve some file from some service
ApplicationFile file = await _fileService.GetFileAsync(id);
var fileProvider = new FileExtensionContentTypeProvider();
// Figures out what the content type should be based on the file name.
if (!fileProvider.TryGetContentType(file.NameWithExtension, out string contentType))
{
throw new ArgumentOutOfRangeException($"Unable to find Content Type for file name {file.NameWithExtension}.");
}
// contentType will be application/pdf for .pdf's, application/vnd.openxmlformats-officedocument.presentationml.presentation for .pptx's, etc.
return File(file.Contents, contentType);
}

 

So there you have it, now you don’t have to maintain some crazy list of Content Type mappings, you can just lean on ASP.NET Core which maintains those mappings for you.

 

Hope this helps.

6 thoughts on “Dynamically setting Content Type in ASP.NET Core with FileExtensionContentTypeProvider

Leave a Reply