Swimburger

Don't use HttpContext.Current, especially when using async

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

HttpContext holds on to all the information regarding the current HTTP request. It has a lot of properties but is most commonly used to get the Request, Response, Session, User, Cache, and more. In ASP.NET WebForms most of the same properties are conveniently provided to you on the Page class. The HttpContext is also available using the Context property on the Page class.
The same goes for MVC controllers. When possible always use the properties made available to you through the Page or Controller.

If for some reason you don't have access to the HttpContext or the properties on the context, you can use static HttpContext.Current which will return the HttpContext for the current HTTP request. This can be very useful, but avoid relying on static state like this, especially when writing asynchronous code or you may run into the following issue in the future.

Static session wrapper gone wrong #

The session is often wrapped by a class to avoid magic strings being used all over your application. The idea is to have const strings at the top of your class and provide typed properties wrapping the session like this:

public static class MyStaticSessionWrapper
{
    private const string CounterKey = "MyCounter";
    private static HttpSessionState Session => HttpContext.Current.Session;

    public static int Counter
    {
        get => (int)(Session[CounterKey] ?? 0);
        set => Session[CounterKey] = value;
    }
}

In this example:

  • A constant string is defined that will be used as the index value to store the counter in session
  • A private static Session property returns the Session property from HttpContext.Current
  • A public static Counter property uses the Session property to set and get the counter integer value. If there's no counter in session, 0 is returned.

I've seen these types of session wrappers many times before, and find them quite useful since they centralize the retrieval, storage, and avoid magic strings.
This also works just fine in ASP.NET WebForms and MVC when using synchronous code. At my latest client, there was a session wrapping class just like this which worked fine for many years.
Until one day NullReferenceException's were thrown left and right. Unfortunately, NullReferenceException's aren't always as obvious as they could be. The exceptions were being logged, but there was no way of knowing which variable or property was null.
It wouldn't even give a line number in the stacktrace. This application was also being load balanced and experiencing session issues in the past due to misconfigured sticky sessions.
So initially, the assumption was that certain session properties were being initialized on one server, but then fetched on a different server leading to a NullReferenceException.

What was actually going wrong is that the static HttpContext.Current was actually null which was unexpected since this had never happened over the years it was used. But this code had never been run inside an asynchronous method before, until now.

Here is an example of a controller which has two actions incrementing the counter stored in session using the above MyStaticSessionWrapper:

public class HomeController : Controller
{
    public ActionResult IncrementWithStaticSession()
    {
        MyStaticSessionWrapper.Counter++;
        return View("Index");
    }

    public async Task<ActionResult> IncrementWithStaticSessionAsync()
    {
        await Task.Run(() =>
        {
            MyStaticSessionWrapper.Counter++;
        });
        return View("Index");
    }
}

One of the actions is incrementing the counter synchronously, and one of the actions is running the same code inside of Task.Run. A simple Task.Run is used to simulate the issue described earlier. The real code was not as clear and simple as this sample.
The synchronous action works as expected, but the asynchronous version throws a NullReferenceException  because HttpContext.Current is null.

Moving away from HttpContext.Current #

Fixing this is relatively easy depending on your codebase. Instead of using HttpContext.Current, use the HttpContext provided as a property on the Page or Controller, or even better, you can simply use the Session property.
You probably do want to keep using a class that wraps the session. Instead of using a static class, use a non-static class and allow the session to be passed in as part of the constructor like this:

public class MySessionWrapper
{
    private const string CounterKey = "MyCounter";
    private readonly HttpSessionStateBase session;

    public MySessionWrapper(HttpSessionStateBase session)
    {
        this.session = session;
    }

    public int Counter
    {
        get => (int)(session[CounterKey] ?? 0);
        set => session[CounterKey] = value;
    }
}

And create a new instance in your controller or have a dependency injection container take care of this for you:

public async Task<ActionResult> IncrementAsync()
{
    await Task.Run(() =>
    {
        var mySession = new MySessionWrapper(Session);
        mySession.Counter++;
    });
    return View("Index");
}

The above code action achieves the same as before but does not cause NullReferenceException's.

Alternatively, you could add extension methods to set and get the counter on the session like this:

public static class MySessionExtensions
{
    private const string CounterKey = "MyCounter";

    public static int GetCounter(this HttpSessionStateBase session) 
        => (int)(session[CounterKey] ?? 0);

    public static void SetCounter(this HttpSessionStateBase session, int count) 
        => session[CounterKey] = count;
}

Unfortunately, C# does not have extension properties yet, so for now you'll have to use the GetX and SetX convention which may not be very appealing to C# developers.
With these extension methods, you can increment the counter asynchronously in an action like this:

public async Task<ActionResult> IncrementWithExtensionMethodsAsync()
{
    await Task.Run(() =>
    {
        Session.SetCounter(Session.GetCounter() + 1);
    });
    return View("Index");
}

Once the usage of HttpContext.Current inside of async code was removed, all the errors were gone.

Related Posts

Related Posts