Speed up legacy ASP.NET applications with HttpContext.Items caching

One of the goals of most websites is to return the page for a user within the fastest time possible. Adding caching of frequently accessed data is a really easy way to do this, however, in legacy applications, there can be a lot of context heavy code making it hard to understand what can be cached, and what can’t.

What I’ve seen over and over again is code that runs the same function multiple times, and within that function using the current users’ information.

        public string GetUserAge()
        {
            var userId = HttpContext.User.Claims.FindFirst(c => c.Name == "UserId");
            var user = context.Users.FirstOrDefault(u => u.Id == userId);
            return user.Age;
        }

In the above block, we’re extracting the UserId from the claims, then getting the whole user from the database and returning the age. This is an example I’ve seen a number of times and it’s easy. It was written with the best of intentions, and when it was first used, a single database hit was absolutely fine. However over time, people have gradually added calls to this function in other places. Those functions are now used in lots of places too.

The biggest example of this is when in a web-page, you start to add multiple components/views. Even worse when it’s a CMS with functional components that aren’t thought about ahead of time. That means that for a single Web Request for a page, that function could be hit 5-10 times, each resulting in a database hit (putting aside that there will be caching if you’re using EntityFramework or other ORMs that support In-Memory entity caching).

So how do we improve the performance of this code that has now become a critical perfomance bottleneck.

Options

Option 1: Rewrite EVERYTHING

Perfectly valid approach, a little nuclear, but if you’ve got this problem, it’s probably in lots of places. That said, this is likely going to be a long-term goal, and not a quick win.

Option 2: Cache all the users

This is normally the solution I see implemented. There will be some kind of global cache, or shared cache like Redis that stores all the users. Alternatively, they’ll be an In-Memory list of the users.
This is, again, a perfectly valid way of doing it. You’re still adding lots of latency, and possibly even serialization costs. On top of that, you’ve also now got a global cache to maintain. It does have the benefit that it reduces the database impact over multiple requests though.

Option 3: Just cache it for the current request.

This is my preferred option as you have only a few lines of code, and now, only a single request to the database for each context based execution (e.g. Web Request).

How?

HttpContext should be familiar to most people in the ASP.NET world. It’s been around since… well I can’t remember so “awhile”. In the full Framework world, it was a static you could reference that had all the properties of the current request, like the URL, the cookies, etc.

One of the under-used parts of the HttpContext object is something called “Items”, with a dictionary that only lives for the life of the current request. This opens up some safe caching options, that will also be fairly memory efficient. Here’s the code above but adding in a Items cache.

        public double GetUserAge()
        {
            if (HttpContext.Current.Items.Contains("UserAge"))
                return (double)HttpContext.Current.Items["UserAge"];

            var userId = HttpContext.Current.User.Claims.FindFirst(c => c.Name == "UserId");
            var user = dbContext.Users.FirstOrDefault(u => u.Id == userId);

            HttpContext.Current.Items["UserAge"] = user.Age;

            return user.Age;
        }

In normal operation, there will likely be no performance degradation, even if there is a single hit, as it’s just adding an item to a dictionary. However, if this is hit multiple times inside a single request, you’ve just reduced the database calls to a single call per-request.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s

Website Built with WordPress.com.

Up ↑

<span>%d</span> bloggers like this: