ASP.NET Web API Updates – April 27

Here’s a few more updates coming your way in ASP.NET Web API. As in the previous update the code has been checked into our CodePlex source code repository but hasn’t yet been released as part of our official installer. Think of this as a sneak-peak for you to try out and comment on as part of our source code repository (see Getting Started With ASP.NET Web Stack Source on CodePlex for details).

Helper for Wiring up Message Handlers

HttpMessageHandlers are pluggable “filters” that sit between HttpClient and the network and allow you to modify the request and/or response as they travel from a HttpClient to the network and back (see Mike Wasson’s blog on message handlers for a detailed walkthrough). HttpMessageHandlers are plugged together in a “Russian doll” model where each handler delegates to the inner handler until the last one hits the network (or a previous one short-circuited the path). The model can be illustrated like this:HttpMessageHandlers1

This commit provides a simple HttpClientFactory that can wire up DelegatingHandlers and create an HttpClient with the desired pipeline ready to go. It also provides functionality for wiring up with alternative inner handlers (the default is HttpClientHandler) as well as do the wiring up when using HttpMessageInvoker or another DelegatingHandler instead of HttpClient as the top-invoker.

To demonstrate this we first create a handler where we override the SendAsync method to intercept the request and response path:

    1: public class MyHandler : DelegatingHandler
    2: {
    3:     private string _name;
    4:  
    5:     public MyHandler(string name)
    6:     {
    7:         _name = name;
    8:     }
    9:  
   10:     // We override the SendAsync method to intercept both the request and response path
   11:     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   12:     {
   13:         Console.WriteLine("Request path in handler {0}", _name);
   14:         return base.SendAsync(request, cancellationToken).ContinueWith(
   15:             requestTask =>
   16:             {
   17:                 Console.WriteLine("Response path in handler {0}", _name);
   18:                 return requestTask.Result;
   19:             }, TaskContinuationOptions.OnlyOnRanToCompletion);
   20:     }
   21: }

Then we use this handler in a sample where we instantiate three instances and wire them together:

    1: // Create three instances of handlers
    2: MyHandler handlerA = new MyHandler("A");
    3: MyHandler handlerB = new MyHandler("B");
    4: MyHandler handlerC = new MyHandler("C");
    5:  
    6: // Create HttpClient with handlers A, B, and C wired up
    7: HttpClient client = HttpClientFactory.Create(handlerC, handlerB, handlerA);
    8:  
    9: // Issue asynchronous GET request
   10: client.GetAsync("https://msdn.microsoft.com").ContinueWith(
   11:     requestTask =>
   12:     {
   13:         if (requestTask.IsCanceled)
   14:         {
   15:             Console.WriteLine("Request was cancelled");
   16:         }
   17:  
   18:         if (requestTask.IsFaulted)
   19:         {
   20:             Console.WriteLine("Request failed with an exception: {0}", requestTask.Exception.GetBaseException());
   21:         }
   22:  
   23:         HttpResponseMessage response = requestTask.Result;
   24:         {
   25:             Console.WriteLine("Request succeeded");
   26:         }
   27:     });

When running this sample we get the following output on the console (note the order with A being first on request and last on response, etc.):

MultipleHandlers

Progress Notifications

Being able to get progress notifications on data being uploaded and downloaded was a popular request on the beta bits which we didn’t support directly. This commit defines a ProgressMessageHandler which generates progress notification for both request entities being uploaded and response entities being downloaded. Using this handler it is possible to keep track of how far you are uploading a request body or downloading a response body.

The ProgressMessageHandler exposes two events, one for tracking upload and download progress respectively. Each progress notification is associated with a specific request so it is possible to track progress notifications from multiple ongoing requests at the same time. A sample receive progress event handler looks like this:

    1: private static void HttpReceiveProgress(object sender, HttpProgressEventArgs e)
    2: {
    3:     // The sender is the originating HTTP request
    4:     HttpRequestMessage request = sender as HttpRequestMessage;
    5:  
    6:     // Write different message depending on whether we have a total length or not
    7:     string message;
    8:     if (e.TotalBytes != null)
    9:     {
   10:         message = String.Format("Request {0} downloaded {1} of {2} bytes ({3}%)", request.RequestUri, e.BytesTransferred, e.TotalBytes, e.ProgressPercentage);
   11:     }
   12:     else
   13:     {
   14:         message = String.Format("Request {0} downloaded {1} bytes", request.RequestUri, e.BytesTransferred, e.TotalBytes, e.ProgressPercentage);
   15:     }
   16:  
   17:     // Write progress message to console
   18:     Console.WriteLine(message);
   19: }

The send progress event handler looks exactly the same – it also receives a HttpProgressEventArgs instance with the exact same shape. Below is a sample which uses the HttpClientFactory to create an HttpClient with progress notifications hooked in:

    1: // Create a progress notification handler and wire up events for upload and download progress
    2: ProgressMessageHandler progress = new ProgressMessageHandler();
    3: progress.HttpReceiveProgress += new EventHandler<HttpProgressEventArgs>(HttpReceiveProgress);
    4: progress.HttpSendProgress += new EventHandler<HttpProgressEventArgs>(HttpSendProgress);
    5:  
    6: // Create a client and wire up the progress handler
    7: HttpClient client = HttpClientFactory.Create(progress);
    8:  
    9: // We can now submit an HTTP request and see the progress notifications in action
   10: client.GetAsync("https://msdn.microsoft.com").ContinueWith(
   11:     (requestTask) =>
   12:     {
   13:         if (requestTask.IsCanceled)
   14:         {
   15:             Console.WriteLine("Request was cancelled");
   16:         }
   17:  
   18:         if (requestTask.IsFaulted)
   19:         {
   20:             Console.WriteLine("Request failed with an exception: {0}", requestTask.Exception.GetBaseException());
   21:         }
   22:  
   23:         HttpResponseMessage response = requestTask.Result;
   24:         {
   25:             Console.WriteLine("Request succeeded");
   26:         }
   27:     });

A sample output written to the console looks like this:

ProgressConsole

Multipart Improvements

Support for reading MIME multipart messages as handled by the HttpContent.ReadAsMultipartAsync extension methods had two usability problems in the original design:

  1. The result was IEnumerable<HttpContent> which is too low-level to facilitate easy consumption. For example, it doesn't allow for converting multipart html form data into NameValueCollection data or for any other higher-level abstraction on top of the raw IEnumerable<HttpContent>.
  2. The stream provider doesn't enable any post processing of the data such as converting the read data into another format before the read task completed.

This made it hard to provide a higher-level abstraction on top of IEnumerable<HttpContent> as the MultipartStreamProvider does not know when the message has been completely read.

This commit fixes these short comings by first changing the signature of the ReadAsMultipartAsync to

  • public static Task<T> ReadAsMultipartAsync<T>(this HttpContent content, T streamProvider) where T : MultipartStreamProvider

This means that it is now possible to write a MultipartStreamProvider that is completely tailored to the type of MIME multipart that it can read and present the result in the optimal way to the user.

Second, we introduce a “post processing” step as part of the MultipartStreamProvider that allows the implementation to do whatever post processing it wants on the MIME multipart body parts. For example, the MultipartFormDataStreamProvider implementation reads the HTML form data parts and adds them to a NameValueCollection so they are easy to get at from the caller.

The built-in set of MultipartStreamProvider consists of:

  1. MultipartMemoryStreamProvider saves all MIME body parts in memory allowing them to be read, deserialized, etc.
  2. MultipartFileStreamProvider saves all MIME body parts as individual files.
  3. MultipartFormDataStreamProvider saves all form data parts in a NameValueCollection and all file parts to file. This allows the form data to be easily manipulated and files to be persisted.
  4. MultipartRelatedStreamProvider parses multipart related entities as defined by RFC 2387 “The MIME Multipart/Related Content-type”.

An example of how to implement an Action that handles file uploads with parameters using MultipartFormDataStreamProvider looks like this:

    1: [HttpPost]
    2: public Task<List<FileResult>> UploadFile()
    3: {
    4:     // Verify that this is an HTML Form file upload request
    5:     if (!Request.Content.IsMimeMultipartContent("form-data"))
    6:     {
    7:         throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.UnsupportedMediaType));
    8:     }
    9:  
   10:     // Create a stream provider for setting up output streams
   11:     MultipartFormDataStreamProvider streamProvider = new MultipartFormDataStreamProvider("d:\\src\\tmp\\uploads");
   12:  
   13:     // Read the MIME multipart asynchronously content using the stream provider we just created.
   14:     return Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(
   15:         readTask =>
   16:         {
   17:             MultipartFormDataStreamProvider provider = readTask.Result;
   18:  
   19:             // Create response containing information about the stored files.
   20:             return provider.FileData.Select(fileData =>
   21:             {
   22:                 FileInfo fileInfo = new FileInfo(fileData.LocalFileName);
   23:                 ContentDispositionHeaderValue disposition = fileData.Headers.ContentDisposition;
   24:                 string filename = (disposition != null && disposition.FileName != null) ? disposition.FileName : String.Empty;
   25:  
   26:                 return new FileResult
   27:                 {
   28:                     FileName = filename,
   29:                     LocalPath = fileInfo.FullName,
   30:                     LastModifiedTime = fileInfo.LastWriteTimeUtc,
   31:                     Length = fileInfo.Length,
   32:                     Submitter = provider.FormData["submitter"]
   33:                 };
   34:             }).ToList();
   35:         });
   36: }

As always please use the CodePlex discussion forum and go vote on your favorite issue in Issue Tracker – we would love to hear you feedback.

Have fun!

Henrik

del.icio.us Tags: asp.net,webapi,httpclient,http