Uploading and returning files in ASP.NET MVC
Index
- 1. Brief
- 2. How to upload a file?
- 3. HttpPostedFileBase
- 4. Uploading multiple files
- 5. Using view models to validate POSTed files
- 6. How to return a file as response?
- 7. Returning files through action results
- 8. Controller helper methods to return files
- 9. Creating custom file action result
- 10. Summary
1. Brief
Uploading and returning files in an ASP.NET MVC application is very simple. The POSTed file(s) are available as parameters directly in actions through model binding. The files in the server can be easily sent as response to the clients through its rich support of action results.
There are already plenty of articles written on this subject. So why another article? Well, in this article I gathered the important concepts that are scattered in different posts, threads in a single place. I'm sure this article will help the MVC programmers to increase their grip on the framework.
Thanks for all the readers who pointed out the errors and typos in the article. I really appreciate them.
2. How to upload a file?
2.1 Basics
POSTing a file to the server is quite simple. All we need is a html form having encoding type set to multipart/form-data and a file input control.
<form action="/profile/upload" method="post" enctype="multipart/form-data"> <label for="photo">Photo:</label> <input type="file" name="photo" id="photo" /> <input type="submit" value="Upload" /> </form>
Listing 1. Simple file upload html form
An important thing developers forget sometimes is setting the encoding type(enctype) to multipart/form-data. As default the encoding type is application/x-www-form-urlencoded which is insufficient for sending large amount of data to server especially when the form contains files, non-ASCII data and binary data.
When you post the form the Content-Type header is set to multipart/form-data. If you forget setting the proper encoding type then only the filename is submitted not the file.
2.2 Reading files from request
The POSTed files are available in HttpFileCollectionBase of Request.Files. In the below listing we can see how to read the POSTed file from the request and save to the server.
[HttpPost] public ActionResult Upload() { string directory = @"D:\Temp\"; HttpPostedFileBase photo = Request.Files["photo"]; if(photo != null && photo.ContentLength > 0) { var fileName = Path.GetFileName(photo.FileName); photo.SaveAs(Path.Combine(directory, fileName)); } return RedirectToAction("Index"); }
Listing 2. Upload action
Instead of manually reading the file from the Request, by taking the advantage of model binding the file can be made directly available as a parameter in the action as shown in the below listing.
[HttpPost] public ActionResult Upload(HttpPostedFileBase photo) { string directory = @"D:\Temp\"; if(photo != null && photo.ContentLength > 0) { var fileName = Path.GetFileName(photo.FileName); photo.SaveAs(Path.Combine(directory, fileName)); } return RedirectToAction("Index"); }
Listing 3. Using HttpPostedFileBase as action parameter
The important thing to note down is the file parameter name should be same as the name of the file input control (in the above case it is photo).
3. HttpPostedFileBase
HttpPostedFileBase is an abstract class that contains the same members as in HttpPostedFile. Prior to the .NET framework 3.5 version there was only HttpPostedFile which is concrete and sealed making the code hard to unit-test. The HttpPostedFileBase is created to substitute HttpPostedFile in MVC applications for better unit testing. So wherever you need to read files, I advise you to use HttpPostedFileBase instead of HttpPostedFile.
An important thing to note down is we can't directly substitute HttpPostedFile with HttpPostedFileBase because HttpPostedFile is still the same, not derives from any type. In some cases we need to convert HttpPostedFileBase to HttpPostedFile and we can achieve that using the HttpPostedFileWrapper.
3.1 Behind the scenes
As many of us already aware, it's the model binding feature that maps the POSTed file to HttpPostedFileBase in the action parameter. But what we are interested here is to know the supporting classes.
The model binding feature relies on two types of components binders and value providers. The value providers are the components that gets the value needed from the particular source (query-strings, form etc.) and feed to binders. The binders are the components that really fills the properties of a model or the parameters in the action with those values.
The MVC framework is designed in such a way that these two components are loosely coupled and hence a binder don't need to worry about which value provider it has to interact to get the value for a property or parameter likewise a value provider don't need to worry about who is asking the value.
3.2 HttpPostedFileBaseModelBinder
When you have a single instance of HttpPostedFileBase as an action parameter or a property in model then mapping the file is completely done by the HttpPostedFileBaseModelBinder and no value providers are used in this case. You may think why no value providers are used in this case, it's because the source is single and clear i.e. Request.Files collection.
3.3 HttpFileCollectionValueProvider
When model binding collection of POSTed files, the HttpFileCollectionValueProvider comes into play along with the DefaultModelBinder. The HttpFileCollectionValueProvider which derives from the DictionaryValueProvider stores all the POSTed files as a dictionary and feeds the DefaultModelBinder with files whenever it needs to map a parameter or property of type HttpPostedFileBase.
4. Uploading multiple files
So uploading a single file and reading it from the server is quite easy, all we need is to set the HttpPostedFileBase type as a parameter in the corresponding action method. How about reading multiple files POSTed to the server? We can easily achieve this by setting an IEnumerable<HttpPostedFileBase> as action parameter.
Here is a sample html form to upload multiple files.
<form action="documents/upload" method="post" enctype="multipart/form-data"> <label for="photo">Photo:</label> <input type="file" name="files[0]" id="files_0" /> <input type="file" name="files[1]" id="files_1" /> <input type="file" name="files[2]" id="files_2" /> <input type="submit" value="Upload" /> </form>
Listing 4. Form to upload multiple files
Below is the action that handles the POST request of the form.
[HttpPost] public ActionResult Upload(IEnumerable<HttpPostedFileBase> files) { foreach(var file in files) { file.SaveAs(...); } return RedirectToAction("Index"); }
Listing 5. Using IEnumerable<HttpPostedFileBase> as action parameter to receive multiple POSTed files
The important thing is the name of the file input controls should match the rules of model binding.
5. Using view models to validate POSTed files
Like any other input data the POSTed files to the server also needs validation. For example, in the case of image we need the file should be one of the supported image types like jpg, jpeg, png by the server and we may also need validations to check the file size, file name etc. before persisting the file in the server.
When we use the HttpPostedFileBase directly as action parameter then we have to validate the file manually as shown in the below listing.
[HttpPost] public ActionResult Upload(HttpPostedFileBase photo) { if (photo != null && photo.ContentLength > 0) { string directory = @"D:\Temp\"; if (photo.ContentLength > 10240) { ModelState.AddModelError("photo", "The size of the file should not exceed 10 KB"); return View(); } var supportedTypes = new[] { "jpg", "jpeg", "png" }; var fileExt = System.IO.Path.GetExtension(photo.FileName).Substring(1); if (!supportedTypes.Contains(fileExt)) { ModelState.AddModelError("photo", "Invalid type. Only the following types (jpg, jpeg, png) are supported."); return View(); } var fileName = Path.GetFileName(photo.FileName); photo.SaveAs(Path.Combine(directory, fileName)); } return RedirectToAction("Index"); }
Listing 6. Uploading file with validations
In the above action we have done couple of validations against the uploaded file. Instead of doing it manually it would be great if you could do that using data annotation attributes and for that we have to use view models.
Lets create a view model that wraps HttpPostedFileBase as a property which is decorated with data annotation attributes.
public class UploadFileModel { [FileSize(10240)] [FileTypes("jpg,jpeg,png")] public HttpPostedFileBase File { get; set; } }
Listing 7. View model
Note that the validation attributes applied over the File property are custom ones and not exists in the data annotations assembly.
Creating custom validation attribute is not a difficult job! All we have to do is derive a class from ValidationAttribute and override the IsValid and FormatErrorMessage methods. Here are our implementations for FileSizeAttribute and FileTypesAttribute.
5.1 FileSizeAttribute
public class FileSizeAttribute : ValidationAttribute { private readonly int _maxSize; public FileSizeAttribute(int maxSize) { _maxSize = maxSize; } public override bool IsValid(object value) { if (value == null) return true; return (value as HttpPostedFileBase).ContentLength <= _maxSize; } public override string FormatErrorMessage(string name) { return string.Format("The file size should not exceed {0}", _maxSize); } }
Listing 8. FileSizeAttribute
5.2 FileTypesAttribute
public class FileTypesAttribute: ValidationAttribute { private readonly List<string> _types; public FileTypesAttribute(string types) { _types = types.Split(',').ToList(); } public override bool IsValid(object value) { if (value == null) return true; var fileExt = System.IO.Path.GetExtension((value as HttpPostedFileBase).FileName).Substring(1); return _types.Contains(fileExt, StringComparer.OrdinalIgnoreCase); } public override string FormatErrorMessage(string name) { return string.Format("Invalid file type. Only the following types {0} are supported.", String.Join(", ", _types)); } }
Listing 9. FileTypesAttribute
Finally we have to replace the action parameter from HttpPostedFileBase to UploadFileModel and the validations will happen automatically when the binding happens. The below listing shows the simplified version of the upload action after using view model.
[HttpPost] public ActionResult Upload(UploadFileModel fileModel) { string directory = @"D:\Temp\"; if(ModelState.IsValid) { if(fileModel != null && fileModel.File != null && fileModel.File.ContentLength > 0) { var fileName = Path.GetFileName(fileModel.File.FileName); fileModel.File.SaveAs(Path.Combine(directory, fileName)); } return RedirectToAction("Index"); } return View(); }
Listing 10. Upload action with view model for validations
So far we have seen how to upload files to server and validate them using data annotations. In the coming sections we will see how we can easily return a file as response to the clients.
6. How to return a file as response?
6.1 How a browser knows what file type is returned from the server?
The Content-Type header is the one that says the browser what kind of file is being returned from the server. For example, to return a pdf file from the server the Content-Type should be set to application/pdf. Likewise to return a png image, the Content-Type should be image/png and so on.
For some content types the browser doesn't open the save dialog and display the content directly inside its window. Example, when you return a pdf file, some browsers knows how to display the pdf files inside it, same for images. For the content-types the browser can't display to the user it opens the save dialog ex. when you return an excel report from the server.
When the user want to save the file sent to the browser, the server can suggest a filename to the client and the Content-Disposition header is just for that.
To return a file from server all we have to do is set the proper Content-Type, Content-Disposition headers and write the file into the response. The below code snippet shows how we can return a file just plain from an action without using action results.
public void GetReport() { Response.ContentType = "application/text"; Response.AddHeader("Content-Disposition", @"filename=""IT Report.xls"""); Response.TransmitFile(@"C:\reports\income_tax_report.xls"); }
Listing 11. Sending an excel report from server
7. Returning files through action results
MVC framework eases the job of returning files through its built-in action results. We don't need to worry about adding any headers in the response the action results will take care.
7.1 FileResult
This is an abstract class derived from ActionResult that delegates writing the file in the response to the subclasses. This class contains a single abstract method called WriteFile that every subclass should implement.
This class mainly does the job of adding Content-Type and Content-Disposition headers into the response. Adding the Content-Type header is not a big deal while determining the value of the Content-Disposition header is not an easy job and the FileResult class uses a private class ContentDispositionUtil for that purpose.
The ContentDispositionUtil tries first to get the header value using the ContentDisposition class which is located in the System.Net.Mime namespace. If it fails then generate the header value based on RFC 2231 from its own methods. To understand how it generates the header see the source code.
There are three built-in classes that implements FileResult: FilePathResult, FileContentResult and FileStreamResult.
7.2 FilePathResult
This action result is used to return a file from the server's physical location.
public FileResult InsuranceReport() { string fileName = @"c:\docs\insurance_report.pdf"; string contentType = "application/pdf"; return new FilePathResult(fileName, contentType); }
Listing 12. Returning file from path
We can even pass a file download name to the FilePathResult,
return new FilePathResult(fileName, contentType) { FileDownloadName = "InsuranceReport.pdf" };
Listing 13. Passing file download name
Internally the FilePathResult class calls the TransmitFile method of HttpResponseBase to send the file in response. You can see the complete code of FilePathResult here.
7.3 FileContentResult
This class is used to return a file from a byte array.
return new FileContentResult(byteArray, "application/pdf");
Listing 14. Returning file from byte array
The FileContentResult writes the complete byte array to the response's OutputStream at-once. You can see the complete code of FileContentResult here.
7.4 FileStreamResult
This action result is used to return a file from a stream.
return new FileStreamResult(fileStream, "application/pdf");
Listing 15. Returning file from stream
The FileStreamResult reads chunks of data from the stream and write into the response. The size of each chunk is 4KB and this can't be changed through code or config. You can see the source code here.
8. Controller helper methods to return files
Actually you don't need to instantiate the FileResult types from action methods the Controller has bunch of built-in methods that helps to easily send a file in response.
The following are the helper methods available in controller.
Helper method | Returning type |
---|---|
File(byte[] fileContents, string contentType) | FileContentResult |
File(byte[] fileContents, string contentType, string fileDownloadName) | FileContentResult |
File(Stream fileStream, string contentType) | FileStreamResult |
File(Stream fileStream, string contentType, string fileDownloadName) | FileStreamResult |
File(string fileName, string contentType) | FilePathResult |
File(string fileName, string contentType, string fileDownloadName) | FilePathResult |
You can rewrite the code shown in Listing 12. using the helper method as below,
public FileResult InsuranceReport() { return File(@"c:\docs\insurance_report.pdf", "application/pdf"); }
Listing 16. Returning file using the helper method
9. Creating custom file action result
We can easily create new file action results by deriving from the the abstract class FileResult. For example, let see how we can create a custom action result that return files from string, let's call it FileStringResult.
public class FileStringResult : FileResult { public string Data { get; set; } public FileStringResult(string data, string contentType) : base(contentType) { Data = data; } protected override void WriteFile(HttpResponseBase response) { if (Data == null) { return; } response.Write(Data); } }
Listing 17. Creating a custom file action result
We can use our FileStringResult as shown in the below action.
public FileResult FinanceReport() { string reportString = ... return new FileStringResult(reportString, "application/text"){ FileDownloadName = "finance-report.csv" }; }
Listing 18. Returning file from string.
10. Summary
In this article we learnt many things about uploading and returning files in an MVC application. We saw how we can apply validations to the POSTed files easily using view models. We discussed about the different types of file action results that helps to return files from the server and even we created a custom file action result that returns a file from string.