Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME
by Mike Wasson
Part 2: File Upload and Multipart MIME
This tutorial shows how to upload files to a web API. It also describes how to process multipart MIME data.
[!NOTE] Download the completed project.
Here is an example of an HTML form for uploading a file:
[!code-htmlMain]
1: <form name="form1" method="post" enctype="multipart/form-data" action="api/upload">
2: <div>
3: <label for="caption">Image Caption</label>
4: <input name="caption" type="text" />
5: </div>
6: <div>
7: <label for="image1">Image File</label>
8: <input name="image1" type="file" />
9: </div>
10: <div>
11: <input type="submit" value="Submit" />
12: </div>
13: </form>
This form contains a text input control and a file input control. When a form contains a file input control, the enctype attribute should always be “multipart/form-data”, which specifies that the form will be sent as a multipart MIME message.
The format of a multipart MIME message is easiest to understand by looking at an example request:
[!code-consoleMain]
1: POST http://localhost:50460/api/values/1 HTTP/1.1
2: User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
3: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
4: Accept-Language: en-us,en;q=0.5
5: Accept-Encoding: gzip, deflate
6: Content-Type: multipart/form-data; boundary=---------------------------41184676334
7: Content-Length: 29278
8:
9: -----------------------------41184676334
10: Content-Disposition: form-data; name="caption"
11:
12: Summer vacation
13: -----------------------------41184676334
14: Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
15: Content-Type: image/jpeg
16:
17: (Binary data not shown)
18: -----------------------------41184676334--
This message is divided into two parts, one for each form control. Part boundaries are indicated by the lines that start with dashes.
[!NOTE] The part boundary includes a random component (“41184676334”) to ensure that the boundary string does not accidentally appear inside a message part.
Each message part contains one or more headers, followed by the part contents.
- The Content-Disposition header includes the name of the control. For files, it also contains the file name.
- The Content-Type header describes the data in the part. If this header is omitted, the default is text/plain.
In the previous example, the user uploaded a file named GrandCanyon.jpg, with content type image/jpeg; and the value of the text input was “Summer Vacation”.
File Upload
Now let’s look at a Web API controller that reads files from a multipart MIME message. The controller will read the files asynchronously. Web API supports asynchronous actions using the task-based programming model. First, here is the code if you are targeting .NET Framework 4.5, which supports the async and await keywords.
[!code-csharpMain]
1: using System.Diagnostics;
2: using System.Net;
3: using System.Net.Http;
4: using System.Threading.Tasks;
5: using System.Web;
6: using System.Web.Http;
7:
8: public class UploadController : ApiController
9: {
10: public async Task<HttpResponseMessage> PostFormData()
11: {
12: // Check if the request contains multipart/form-data.
13: if (!Request.Content.IsMimeMultipartContent())
14: {
15: throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
16: }
17:
18: string root = HttpContext.Current.Server.MapPath("~/App_Data");
19: var provider = new MultipartFormDataStreamProvider(root);
20:
21: try
22: {
23: // Read the form data.
24: await Request.Content.ReadAsMultipartAsync(provider);
25:
26: // This illustrates how to get the file names.
27: foreach (MultipartFileData file in provider.FileData)
28: {
29: Trace.WriteLine(file.Headers.ContentDisposition.FileName);
30: Trace.WriteLine("Server file path: " + file.LocalFileName);
31: }
32: return Request.CreateResponse(HttpStatusCode.OK);
33: }
34: catch (System.Exception e)
35: {
36: return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
37: }
38: }
39:
40: }
Notice that the controller action does not take any parameters. That’s because we process the request body inside the action, without invoking a media-type formatter.
The IsMultipartContent method checks whether the request contains a multipart MIME message. If not, the controller returns HTTP status code 415 (Unsupported Media Type).
The MultipartFormDataStreamProvider class is a helper object that allocates file streams for uploaded files. To read the multipart MIME message, call the ReadAsMultipartAsync method. This method extracts all of the message parts and writes them into the streams provided by the MultipartFormDataStreamProvider.
When the method completes, you can get information about the files from the FileData property, which is a collection of MultipartFileData objects.
- MultipartFileData.FileName is the local file name on the server, where the file was saved.
- MultipartFileData.Headers contains the part header (not the request header). You can use this to access the Content_Disposition and Content-Type headers.
As the name suggests, ReadAsMultipartAsync is an asynchronous method. To perform work after the method completes, use a continuation task (.NET 4.0) or the await keyword (.NET 4.5).
Here is the .NET Framework 4.0 version of the previous code:
[!code-csharpMain]
1: public Task<HttpResponseMessage> PostFormData()
2: {
3: // Check if the request contains multipart/form-data.
4: if (!Request.Content.IsMimeMultipartContent())
5: {
6: throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
7: }
8:
9: string root = HttpContext.Current.Server.MapPath("~/App_Data");
10: var provider = new MultipartFormDataStreamProvider(root);
11:
12: // Read the form data and return an async task.
13: var task = Request.Content.ReadAsMultipartAsync(provider).
14: ContinueWith<HttpResponseMessage>(t =>
15: {
16: if (t.IsFaulted || t.IsCanceled)
17: {
18: Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
19: }
20:
21: // This illustrates how to get the file names.
22: foreach (MultipartFileData file in provider.FileData)
23: {
24: Trace.WriteLine(file.Headers.ContentDisposition.FileName);
25: Trace.WriteLine("Server file path: " + file.LocalFileName);
26: }
27: return Request.CreateResponse(HttpStatusCode.OK);
28: });
29:
30: return task;
31: }
Reading Form Control Data
The HTML form that I showed earlier had a text input control.
[!code-htmlMain]
1: <div>
2: <label for="caption">Image Caption</label>
3: <input name="caption" type="text" />
4: </div>
You can get the value of the control from the FormData property of the MultipartFormDataStreamProvider.
[!code-csharpMain]
1: public async Task<HttpResponseMessage> PostFormData()
2: {
3: if (!Request.Content.IsMimeMultipartContent())
4: {
5: throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
6: }
7:
8: string root = HttpContext.Current.Server.MapPath("~/App_Data");
9: var provider = new MultipartFormDataStreamProvider(root);
10:
11: try
12: {
13: await Request.Content.ReadAsMultipartAsync(provider);
14:
15: // Show all the key-value pairs.
16: foreach (var key in provider.FormData.AllKeys)
17: {
18: foreach (var val in provider.FormData.GetValues(key))
19: {
20: Trace.WriteLine(string.Format("{0}: {1}", key, val));
21: }
22: }
23:
24: return Request.CreateResponse(HttpStatusCode.OK);
25: }
26: catch (System.Exception e)
27: {
28: return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
29: }
30: }
FormData is a NameValueCollection that contains name/value pairs for the form controls. The collection can contain duplicate keys. Consider this form:
[!code-htmlMain]
1: <form name="trip_search" method="post" enctype="multipart/form-data" action="api/upload">
2: <div>
3: <input type="radio" name="trip" value="round-trip"/>
4: Round-Trip
5: </div>
6: <div>
7: <input type="radio" name="trip" value="one-way"/>
8: One-Way
9: </div>
10:
11: <div>
12: <input type="checkbox" name="options" value="nonstop" />
13: Only show non-stop flights
14: </div>
15: <div>
16: <input type="checkbox" name="options" value="airports" />
17: Compare nearby airports
18: </div>
19: <div>
20: <input type="checkbox" name="options" value="dates" />
21: My travel dates are flexible
22: </div>
23:
24: <div>
25: <label for="seat">Seating Preference</label>
26: <select name="seat">
27: <option value="aisle">Aisle</option>
28: <option value="window">Window</option>
29: <option value="center">Center</option>
30: <option value="none">No Preference</option>
31: </select>
32: </div>
33: </form>
The request body might look like this:
[!code-consoleMain]
1: -----------------------------7dc1d13623304d6
2: Content-Disposition: form-data; name="trip"
3:
4: round-trip
5: -----------------------------7dc1d13623304d6
6: Content-Disposition: form-data; name="options"
7:
8: nonstop
9: -----------------------------7dc1d13623304d6
10: Content-Disposition: form-data; name="options"
11:
12: dates
13: -----------------------------7dc1d13623304d6
14: Content-Disposition: form-data; name="seat"
15:
16: window
17: -----------------------------7dc1d13623304d6--
In that case, the FormData collection would contain the following key/value pairs:
- trip: round-trip
- options: nonstop
- options: dates
- seat: window
|