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 fromHttpContext.Current
is returned by a private static Session property. - The
Session
property is used to set and retrieve the integer value of a public staticcounter
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.
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.