DEV Community

loading...

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

Niels Swimberghe
Belgian Full Stack Developer solving problems and delivering value to customers using .NET technologies for back-end systems, and modern JavaScript technologies for the front-end.
Originally published at swimburger.net on ・4 min read

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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");
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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");
}

Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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");
}

Enter fullscreen mode Exit fullscreen mode

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

Discussion (2)

Collapse
mdora5251 profile image
mdora5251

I faced similar issues with HttpContext under the MVC namespaces. May this not regarding of your topic. I was aware that; HttpContext in MVC namespace and HttpContext within the System.Web namespace is not the same behavior. Because sometimes HttpContext within the MVC namespace getting null exception

Collapse
swimburger profile image
Niels Swimberghe Author

Interesting, I wasn't aware of that.