AVOID Using HttpContext.Current Especially When Using Async

All the data pertaining to the active HTTP request is stored in the HTTPContext object. Although it has many properties, the Request, Response, Session, User, Cache, and other things are most frequently retrieved from it. The Page class in ASP.NET WebForms conveniently offers you access to the majority of the same properties. The Page class’s Context property also has a HttpContext option. This also applies to MVC controllers. Use the properties that the Page or Controller has made available to you whenever possible.

Static HttpContext.Current will return the HttpContext for the current HTTP request if, for some reason, you don’t have access to the HttpContext or the properties on the context. While using static state in this way can be very helpful, you should avoid doing so, especially when writing asynchronous code, as you may later encounter the following problem.

Incorrect Static Session Wrapper

The session is frequently wrapped by a class to prevent the use of magic strings throughout your application. The idea is to provide typed properties and place const strings at the top of your class, wrapping the session as follows:

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:

  • The index value for storing the counter in session is defined as a constant string.
  • The Session property from HttpContext.Current is returned by a private static Session property.
  • The Session property is used to set and retrieve the integer value of a public static counter property. 0 is returned if there isn’t a counter in the session.

These session wrappers are a common type that I’ve seen before and find to be very helpful because they centralize retrieval, storage, and avoid magic strings.

NullReferenceExceptions were being thrown left and right up until that point. NullReferenceExceptions, regrettably, aren’t always as obvious as they ought to be. Although the exceptions were being recorded, it was impossible to determine which variable or property was null.

The stacktrace wouldn’t even include a line number. In the past, this application was load balanced and had issues with sessions because sticky sessions were configured incorrectly.When using synchronous code, this also performs flawlessly in ASP.NET WebForms and MVC. A similar session wrapping class existed at my most recent client and was successful for many years.

In the beginning, it was assumed that some session properties were being initialized on one server, then fetched on another server, resulting in a NullReferenceException.

The static HttpContext.Current was actually null, which was unexpected because it had never happened over the years it had been used, and this was what was actually wrong. But up until this point, this code had never been executed within an asynchronous method.

Here is an illustration of a controller that uses the aforementioned MyStaticSessionWrapper and has two actions that increase the session-stored counter:

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 involves synchronously increasing the counter, and another involves running the same code inside of Task.Run. An easy task.Run is used to model the earlier mentioned problem. Real code was not as unambiguous and straightforward as this example.

Because HttpContext.Current is null, the asynchronous action fails and the synchronous action performs as expected.

Abandoning HttpContext.Current

Depending on your codebase, fixing this is probably not too difficult. rather than HttpContext.Use the HttpContext property that is currently provided on the Page or Controller, or even better, just use the Session property.

Most likely, you should continue using a class that wraps the session. Use a non-static class rather than a static one, and allow the session to be passed as part of the constructor as in the following example:

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

Additionally, make a new instance in your controller or delegate this to a dependency injection container:

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

The aforementioned code snippet accomplishes the same thing as before without resulting in NullReferenceExceptions.

As an alternative, you could include extension methods to set and retrieve the session’s counter in the following way:

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

You will currently have to use the GetX and SetX convention, which may not be very appealing to C# developers, as C# does not yet support extension properties.

With the help of these extension methods, you could perform the following action to increment the counter asynchronously:

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

All errors vanished as soon as HttpContext.Current was removed from async code.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *