Better than HttpContext.Items

Old dogs know old tricks.

For as long as I can remember writing Asp.Net applications (which is over 15 years) I've been using the HttpContext.Items collection as a place to store values I wanted to share across different parts of a single request.

The pattern was pretty simple and effective. In some early part of a request I would retrieve and save values like the following.

MyModel myobj = myService.GetValue(...);

HttpContext.Items[Constants.MyObj] = myobj;

Then later on in my controller or some other service class I would retrieve and use the value.

var myobj = (MyModel) HttpContext.Items[Contants.MyObj];

var query = dbContext.Users.Where(x => x.tag == myobj.Tag);
Pages/Index.cshtml.cs

Pretty simple and it works. The casting and checking for the existence of the key can get old but that could be avoided by hiding it all behind a service façade.

The Old Trick

This last week I had a situation where I wanted to check each request for the existence of a cookie and if the cookie existed preload some user profile data. So, my old mind immediately went to HttpContext.Items.  So, I wrote some middleware similar to this.

public class SessionMiddleware
{
    private readonly RequestDelegate _next;

    public SessionMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
   	public async Task InvokeAsync(HttpContext context, ApplicationDbContext dbContext)
    {   
        if (context.User.Identity.IsAuthenticated)
        {
            if (context.Request.Cookies.TryGetValue("key", out var id))
            {
                var item = await dbContext.Items.SingleOrDefaultAsync(x => x.Id == id);
                context.Items["key"] = item;
            }
        }
        
        await _next(context);
    }
}

Then in my page I would use the value like this.

Item item = null;

if(HttpContext.Items.ContainsKey("key")){
  item = (Item)HttpContext.Items["key"];
}

...

This worked great but  during code review I realized, or rather remembered that there is a better and cleaner way to accomplish this feature.

The New Trick

Asp.Net Core has dependency injection built in at its foundation and supports three lifetimes: Transient, Scoped and Singleton.

Singleton scoped services are created once for an entire application, a new instance of a Transient service is created each time it is injected and Scoped services are created once per client request.

The HttpContext.Items collection is  created once per request so we can replace it with a scoped service.

In addition to having the same per request lifetime a scoped service can be strongly typed and make use of other services.

To make use of  scope services I had to first create and register my service class.

public class ApplicationContextService{

  public Item SelectedItem {get; set;}
  
}
Services/ApplicationContextService.cs
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddRazorPages();
    services.AddScoped<ApplicationContextService, ApplicationContextService>();
  }
}
Startup.cs

Then I simply had to updated the InvokeAsync method on my middleware class and page to use the new service.

public async Task InvokeAsync(HttpContext context, ApplicationContextService applicationContext, ApplicationDbContext dbContext)
{   
  if (context.User.Identity.IsAuthenticated)
  {
    if (context.Request.Cookies.TryGetValue("key", out var id))
    {
      applicationContext.SelectedItem = await dbContext.Items.SingleOrDefaultAsync(x => x.Id == id);
    }
  }
      
  await _next(context);
}
Middleware/SessionMiddleware.cs

public class Index: PageModel{
  
  private ApplicationContextService _applicationContext;
  
  public Index(ApplicationContextService applicationContext){
    _applicationContext = applicationContext;
  }
  
  public async Task<IActionResult> OnGetAsync(){
    var item = _applicationContext.SelectedItem;
    // use the item
  }
}
Pages/Index.cshtml.cs

Not only is this approach cleaner but it removes the need for my page class to know that a value is stored in HttpContext.Items or what the key is.

Conclusion

I've been programming for over 20 years and I still need to remind myself that the first solution to a problem isn't always the best one.  This situation is especially eye opening for me because I've used scoped services many, many, many times before but for some reason it slipped my mind. My old mind remembered an old trick and I didn't think of the new trick until I was reviewing the code.  (Bonus lesson: always review your code even after it's working).

Other Scenarios

Example code is rarely complete. So, here are some ways we could expand or improve on this topic.

  • Have ApplicationContextService take a dependency on the ApplicationDbContext and lazy load the selected item
  • Remove the middleware class by having ApplicationContextService take a dependency on IHttpContextAccessor
  • Support changing the selected item in the ApplicationContextService

Learn More

Cover image by Dasha Urvachova on Unsplash