Asynchronous File Upload using ASP.NET Web API

HTML Form File upload (defined in RFC 1867) is a classic mechanism for uploading content to a Web server and is supported by all browsers that I am aware of. This blog shows how to handle Form File Upload in ASP.NET Web API asynchronously using the Task-based pattern introduced in .NET 4 and enhanced in .NET 4.5.

Using ASP.NET Web API you can upload files of any size when self hosted (well, up to System.Int64.MaxValue which in practice is “any size”). ASP.NET has a maximum limit of 2G in terms of file size that you can upload.

What is HTML File Upload?

Let’s first remind ourselves what HTML File Upload is. If you don’t need to brush up on HTML file upload then you can just skip to the next section…

You enable support for HTML File Upload in an HTML form by using the attribute enctype=”multipart/form-data” and then have a input field of type “file” like this:

    1: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    2: <html>
    3: <head>
    4:     <title>File Upload Sample</title>
    5: </head>
    6: <body>
    7:     <form action="https://localhost:8080/api/upload" enctype="multipart/form-data" method="POST">
    8:     What is your name?
    9:     <input name="submitter" size="40" type="text"><br>
   10:     What file are you uploading?
   11:     <input name="data" size="40" type="file">
   12:     <br>
   13:     <input type="submit">
   14:     </form>
   15: </body>
   16: </html>

This will cause all the data to be encoded using MIME multipart as follows when submitted in an HTTP POST request:

    1: Content-type: multipart/form-data, boundary=AaB03x
    2:  
    3: --AaB03x
    4: content-disposition: form-data; name="submitter"
    5:  
    6: Henrik Nielsen
    7: --AaB03x
    8: content-disposition: form-data ; name="data"; filename="file1.txt"
    9: Content-Type: text/plain
   10:  
   11:  ... contents of file1.txt ...
   12: --AaB03x--

Note how the input field names in the form are mapped to a Content-Disposition header in the MIME multipart message. Each form field such as the submitter field above is encoded as its own MIME body part (that is the term for each segment between the boundaries -- above it is the string –AaB03x).

In HTML5 many browsers support upload of multiple files within a single form submission using the multiple keyword:

    1: <!DOCTYPE HTML>
    2: <html>
    3: <head>
    4:     <title>HTML5 Multiple File Upload Sample</title>
    5: </head>
    6: <body>
    7:     <form action="https://localhost:8080/api/upload" enctype="multipart/form-data" method="POST">
    8:     What is your name?
    9:     <input name="submitter" size="40" type="text"><br>
   10:     What files are you uploading?
   11:     <input name="data" type=file multiple>
   12:     <br>
   13:     <input type="submit" />
   14:     </form>
   15: </body>
   16: </html>
   17:  

The principle in submitting the data is the same as for HTML 4 but you can now select multiple files that each end up in their own MIME body part.

Creating an ApiController

First we create an ApiController that implements an HTTP POST action handling the file upload. Note that the action returns Task<T> as we read the file asynchronously.

Note: We use the new async/await keywords introduced in Visual Studio 11 Beta but you can equally well use Tasks and the ContinueWith pattern already present in Visual Studio 2010.

The first thing we do is check that the content is indeed “multipart/form-data” . The second thing we do is creating a MultipartFormDataStreamProvider which gives you control over where the content ends up. In this case we save the file in the folder “c:\tmp\uploads”. It also contains information about the files stored.

If you want complete control over how the file is written and what file name is used then you can derive from MultipartFormDataStreamProvider, override the functionality you want and use that StreamProvider instead.

Once the read operation has completed we check the at is done we read the content asynchronously and when that task has completed we generate a response containing the submitter and the file names we use on the server, Obviously this is not a typical response but this is just so that you can see the information.

    1: public class UploadController : ApiController
    2: {
    3:     public async Task<List<string>> PostMultipartStream()
    4:     {
    5:         // Verify that this is an HTML Form file upload request
    6:         if (!Request.Content.IsMimeMultipartContent("form-data"))
    7:         {
    8:             throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    9:         }
   10:  
   11:         // Create a stream provider for setting up output streams that saves the output under c:\tmp\uploads
   12:         // If you want full control over how the stream is saved then derive from MultipartFormDataStreamProvider
   13:         // and override what you need.
   14:         MultipartFormDataStreamProvider streamProvider = new MultipartFormDataStreamProvider("c:\\tmp\\uploads");
   15:  
   16:         // Read the MIME multipart content using the stream provider we just created.
   17:         IEnumerable<HttpContent> bodyparts = await Request.Content.ReadAsMultipartAsync(streamProvider);
   18:  
   19:         // The submitter field is the entity with a Content-Disposition header field with a "name" parameter with value "submitter"
   20:         string submitter;
   21:         if (!bodyparts.TryGetFormFieldValue("submitter", out submitter))
   22:         {
   23:             submitter = "unknown";
   24:         }
   25:  
   26:         // Get a dictionary of local file names from stream provider.
   27:         // The filename parameters provided in Content-Disposition header fields are the keys.
   28:         // The local file names where the files are stored are the values.
   29:         IDictionary<string, string> bodyPartFileNames = streamProvider.BodyPartFileNames;
   30:  
   31:         // Create response containing information about the stored files.
   32:         List<string> result = new List<string>();
   33:         result.Add(submitter);
   34:  
   35:         IEnumerable<string> localFiles = bodyPartFileNames.Select(kv => kv.Value);
   36:         result.AddRange(localFiles);
   37:  
   38:         return result;
   39:     }
   40: }

In the above code we added an extension method for getting the value of the submitter form field as a string. That extension method looks like this (don’t worry, we will integrate this better):

    1: public static bool TryGetFormFieldValue(this IEnumerable<HttpContent> contents, string dispositionName, out string formFieldValue)
    2: {
    3:     if (contents == null)
    4:     {
    5:         throw new ArgumentNullException("contents");
    6:     }
    7:  
    8:     HttpContent content = contents.FirstDispositionNameOrDefault(dispositionName);
    9:     if (content != null)
   10:     {
   11:         formFieldValue = content.ReadAsStringAsync().Result;
   12:         return true;
   13:     }
   14:  
   15:     formFieldValue = null;
   16:     return false;
   17: }

Hosting the Controller

In this example we build a simple console application for hosting the ApiController while setting the MaxReceivedMessageSize and TransferMode in the HttpSelfHostConfiguration. We then use a simple HTML form to point to the ApiController so that we can use a browser to upload a file. That HTML form can be hosted anywhere – here we just drop it into the folder C:\inetpub\wwwroot\Samples and serve it using IIS.

    1: class Program
    2: {
    3:     static void Main(string[] args)
    4:     {
    5:         var baseAddress = "https://localhost:8080/";
    6:         HttpSelfHostServer server = null;
    7:  
    8:         try
    9:         {
   10:             // Create configuration
   11:             var config = new HttpSelfHostConfiguration(baseAddress);
   12:  
   13:             // Set the max message size to 1M instead of the default size of 64k and also
   14:             // set the transfer mode to 'streamed' so that don't allocate a 1M buffer but 
   15:             // rather just have a small read buffer.
   16:             config.MaxReceivedMessageSize = 1024 * 1024;
   17:             config.TransferMode = TransferMode.Streamed;
   18:  
   19:             // Add a route
   20:             config.Routes.MapHttpRoute(
   21:               name: "default",
   22:               routeTemplate: "api/{controller}/{id}",
   23:               defaults: new { controller = "Home", id = RouteParameter.Optional });
   24:  
   25:             server = new HttpSelfHostServer(config);
   26:  
   27:             server.OpenAsync().Wait();
   28:  
   29:             Console.WriteLine("Hit ENTER to exit");
   30:             Console.ReadLine();
   31:         }
   32:         finally
   33:         {
   34:             if (server != null)
   35:             {
   36:                 server.CloseAsync().Wait();
   37:             }
   38:         }
   39:     }
   40: }

Trying it Out

Start the console application, then point your browser at the HTML form, for example https://localhost/samples/uploadsample.html .

Once you have uploaded a file you will find it in the folder c:\\tmp\uploads and also get a content negotiated result which if asking for JSON looke like this:

["henrik","c:\\tmp\\uploads\\sample.random"]

Have fun!

Henrik

del.icio.us Tags: asp.net,webapi,mvc,rest,http