Skip to content

Implementing async HTTP modules in ASP.NET using TPL’s Task API

July 27, 2013

Most people familiar with ASP.NET’s HTTP modules are used to implementing the synchronous pipeline APIs (events such as BeginRequest, AuthenticateRequest, EndRequest, etc.). Far fewer people are aware that there are also asynchronous versions of these events that an author of an HTTP module can implement. These async APIs are useful when your interception code in the module needs to perform some sort of I/O bound work. This allows your HTTP module to relinquish its thread while waiting on the results of the I/O bound work. The end result is that you don’t waste threads in the ASP.NET thread pool by blocking.

An example of one of these APIs is AddOnBeginRequestAsync. This accepts two callbacks (a “Begin” and a “End”) and follows the .NET v1.1 APM (asynchronous programming model) style for asynchronous work. It looks like this:


public void AddOnBeginRequestAsync(
    BeginEventHandler bh,
    EndEventHandler eh
)

The BeginEventHandler would return the APM-style IAyncResult.Here’s the start of a HTTP module that implements this:

public class MyModule : IHttpModule
{
   public void Dispose()
   {
   }

   public void Init(HttpApplication app)
   {
      app.AddOnBeginRequestAsync(OnBegin, OnEnd);
   }

   private IAsyncResult OnBegin(object sender, EventArgs e, AsyncCallback cb, object extraData)
   {
      ...
   }

   private void OnEnd(IAsyncResult ar)
   {
      ...
   }
}

If the underlying APIs you use to implement OnBegin function are also APM style, then you can just return their IAyncResult return values, as such:

WebRequest req;
private IAsyncResult OnBegin(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
   req = System.Net.HttpWebRequest.Create("http://foo.com");
   return req.BeginGetResponse(cb, extraData);
}

private void OnEnd(IAsyncResult ar)
{
   var resp = req.EndGetResponse(ar);
   // use resp here...
}

But given that the modern API for modeling asynchronous operations is the Task, then it’s possible you’d want to use a Task-based API and the new async/await features in C# to implement your I/O bound work. How can we bridge these two programming models? Fortunately, it’s pretty boilerplate:


private IAsyncResult OnBegin(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
   var tcs = new TaskCompletionSource<object>(extraData);
   DoAsyncWork(HttpContext.Current).ContinueWith(t =>
   {
      if (t.IsFaulted)
      {
         tcs.SetException(t.Exception.InnerExceptions);
      }
      else
      {
         tcs.SetResult(null);
      }
      if (cb != null) cb(tcs.Task);
   });
   return tcs.Task;
}

private void OnEnd(IAsyncResult ar)
{
   Task t = (Task)ar;
   t.Wait();
}

async Task DoAsyncWork(HttpContext ctx)
{
   var client = new HttpClient();
   var result = await client.GetStringAsync("http://foo.com");
   // use result
}

The real async work we want to do is in DoAsyncWork and it can take advantage of async/await and any other Task-based API. The rest of the code is the boilerplate code (inside of OnBegin and OnEnd).

For the return value of OnBegin we don’t want to implement IAsyncResult ourselves, so we leverage the fact that Task class already does so. We use TaskCompleationSource to obtain a Task because it 1) allows us to set the extraData on the Task that implements the IAsyncResult, and 2) it gives us a reference to the Task to pass when we need to invoke the callback. Our return value from OnBegin is the IAsyncResult as implemented by the Task in our TaskCompletionSource.

As you can see from the code, we invoke the actual async work and use a continuation to handle the outcome. In the continuation we check if the work had an exception or not. If so, we set the TaskCompleationSource into an exception state, otherwise we set it into a completed state (by passing null). We then need to invoke the callback (as per the APM style) to let ASP.NET know the work is done as we pass the same IAsyncResult instance that we returned from OnBegin (this is a subtle point and is why we don’t use async/await on the OnBegin itself).

OnEnd is also boilerplate. We need to block if the async work is not complete, so we downcast the IAsyncResult to our Task and call Wait. This also has the nice side-effect of re-throwing any exception that occurred (as set by SetException in the continuation described above).

The above code shows how to write the boilerplate code and is what you’d have to do if you’re still in .NET 4.0. There’s another easier approach if you’re in .NET 4.5. You can use the the EventHandlerTaskAsyncHelper class and it wraps up the boilerplate code for you (but requires your async method to accept an event-style set of params):

public void Init(HttpApplication app)
{
   var wrapper = new EventHandlerTaskAsyncHelper(DoAsyncWork);
   app.AddOnBeginRequestAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler);
}

async Task DoAsyncWork(object sender, EventArgs e)
{
   var app = (HttpApplication)sender;
   var ctx = app.Context;

   var client = new HttpClient();
   var result = await client.GetStringAsync("http://foo.com");
   // use result
}

Hopefully with this boilerplate code you will have an easy time if you ever need if you ever need to implement an async HTTP module.

4 Comments leave one →
  1. December 25, 2014 2:35 am

    Thanks a lot for this article. It helps me in understanding Http Modules and implementing it in my project. :)

  2. arunjai permalink
    December 25, 2014 2:36 am

    Thanks a lot for this Article.

  3. September 2, 2016 4:53 pm

    Will the task-based implementation block in the OnEnd method?

    private void OnEnd(IAsyncResult ar)
    {
    Task t = (Task)ar;
    t.Wait();
    }

    My concern is t.Wait() will block until the task is complete. Or perhaps the task is always complete when OnEnd is invoked? In this case the t.Wait() call is unnecessary.

    • May 4, 2017 5:21 pm

      To be honest, at this point I don’t recall the details — IIRC there’s a condition in which OnEnd is called earlier than you’d expect, and so you must not return until it’s done. Makes me feel old that I can’t remember…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: