With ASP.NET Core, developers have several new tools to build their web applications. One feature that I am incredibly excited about is request features.

In ASP.NET Core, a request feature is an entity created during a user’s HTTP request. The entity is generally created within middleware for the purpose of allowing developers to enhance or modify the current application’s hosting pipeline.

In this post, I will show you a few things:

  1. How to create a request feature
  2. How to register it within your application’s hosting pipeline
  3. How to retrieve it for use within any subsequent pipeline element
  4. Mutate the request feature.

Before we get started with a code sample, let’s talk about some of the features ASP.NET Core comes with by default.

Download the project now

What Is A Request Feature

As mentioned above, a request feature is a class that helps developers enhance or modify the current application’s hosting pipeline. These features are accessible via the HttpContext of an incoming request.

HttpContext.Features.Get<MyAwesomeFeature>();

Features can be any class you implement and generally regarded as mutable and scoped within the lifetime of a single user request. ASP.NET Core has default features that include functions around authentication, request accessors, file manipulation, and session management. To get a complete list, I recommend the Microsoft documentation site.

One of the more exciting features mentioned in the Microsoft documentation is the IHttpSendFileFeature .

If the feature exists, it’s used to send the requested static file from its physical path. Otherwise, a slower alternative method is used to send the file. When available, the IHttpSendFileFeature allows the operating system to open the file and perform a direct kernel mode copy to the network card. Microsoft Docs

As you may notice reading through the documentation, Microsoft predominantly uses request features for low-level HTTP manipulation. Don’t let the documentation fool you; request features are not just limited to that.

How To Create A Request Feature

A request feature is any class you can imagine. I do have some recommendations around what you should consider as a request feature:

  1. Is feature data generally stable or even read-only?
  2. Is feature data used in many places throughout my request pipeline?
  3. Is the feature data dependant on an HTTP element such as cookies, query strings, or headers?

In this post, I’ll be building a Clock feature. This feature will allow us to set the time zone of our user and then pass that feature through or pipeline. Let’s look at our class.

public class Clock
{
    public DateTimeOffset DateTime { get; set; }
        = DateTimeOffset.UtcNow;

    public TimeZoneInfo TimeZone { get; set; }
        = TimeZoneInfo.Utc;

    public DateTimeOffset Local =>
        TimeZoneInfo.ConvertTimeBySystemTimeZoneId(DateTime, TimeZone?.Id ?? TimeZoneInfo.Utc.Id);
}

We are utilizing TimeZoneInfo and DateTimeOffset to help us determine the local time of our user’s timezone.

If we take a step back and think about the questions I posed above, we can answer them.

  • Is feature data generally stable or even read-only? Yes, a user’s time zone does not change frequently once selected.
  • Is feature data used in many places throughout my request pipeline? Most likely. If I were building a data application, I might want to know a user’s time zone to convert application times to relevant user times appropriately.
  • Is the feature data dependant on an HTTP element such as cookies, query strings, or headers? We’ll see in the next section, but yes! We are storing the timezone in a cookie. Even if we weren’t, we’d most likely be using a user identifier from the current user.

How to Register A Request Feature

As you may have read earlier, we register features within a middleware. For this example, we will need to write a ClockMiddleware.

public class ClockMiddleware : IMiddleware
{
    public const string TimeZoneKey = "clock.timezone";

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var clock = new Clock();
        if (context.Request.Cookies.TryGetValue(TimeZoneKey, out var timezone))
        {
            clock.TimeZone = TimeZoneInfo.FindSystemTimeZoneById(timezone)
                ?? TimeZoneInfo.Utc;
        }

        // set the feature
        context.Features.Set(clock);
        await next(context);
    }

    public static void SetTimeZone(HttpResponse response, string timeZoneId)
    {
        response.Cookies.Append(TimeZoneKey, timeZoneId);
    }
}

This middleware will instantiate a Clock and push it into the Features collection that hangs from our HttpContext. Additionally, if the current user has already set their time zone and has a cookie, we will change our feature to reflect the current time zone.

To register our middleware, we add it to the application’s pipeline.

app.UseMiddleware<ClockMiddleware>();

As a side note, I used the IMiddleware interface to create my middleware. The use of the interface means creation occurs with the help of the MiddlewareFactory. To make our example work, you will need to register our middleware within the services collection of our application.

// this is necessary because we are using
// the IMiddleware interface and the Middleware factory
services.AddScoped<ClockMiddleware>();

You can read more about middleware at the Microsoft documentation site.

Retrieve a Request Feature

In my example, I’ve built a simple Razor Pages application. The app allows us to see the local time of any time zone as it relates to the current moment. Select the time zone; see the local time.

public void OnGet()
{
    Clock = HttpContext.Features.Get<Clock>();
    TimeZone = Clock.TimeZone;
    TimeZoneId = Clock.TimeZone.Id;
}

By the time we reach the Razor page, our middleware has already instantiated our Clock and added it to the features collection. It is just a matter of asking for the feature.

Mutate the Request feature.

There are several ways to mutate a request feature. Since request features should be limited to the scope of a user request, you could manipulate the properties directly. In the case of our ClockMiddleware, we know we are reading from an HTTP cookie.

public IActionResult OnPost()
{
    ClockMiddleware.SetTimeZone(Response, TimeZoneId);
    return RedirectToPage("Index");
}

On the following request, our Clock feature should reflect the updated time zone.

Here is the application in action.

working asp.net core request feature sample

Pros and Cons Of Request Features

While being new to ASP.NET Core, the request feature collection does look similar to something we’ve had for a long time already: Dependency Injection. The sample I gave could be implemented using an Inversion Of Control (IoC) library. That said, I hope I can convince you to consider a request feature over IoC in some cases by listing the Pros and Cons.

Pros of Request Features

The most compelling reason to use Request Features is the way you can instantiate a feature. Using a piece of middleware to create an object means you can more clearly utilize the incoming HTTP request, and potentially the response.

If you were to attempt the same code with an IoC library, you would likely end up writing complicated factories with a myriad of dependencies.

The other compelling reason is it is evident that features are tied to the request context and scoped to the user. Request features can help reduce captured dependencies or cross-threading issues (but not eliminate them).

Finally, my favorite is performance. If your feature is read in many places but doesn’t change frequently, then it makes sense to instantiate early and once. Doing the work upfront in your pipeline reduces unnecessary network calls, object allocations, and more.

Cons of Request Features

The approach can very much feel like the service locator pattern, which shunned by many IoC practitioners.

The service locator pattern is a design pattern or anti-pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the “service locator,” which on request returns the information necessary to perform a certain task.–Wikipedia

Personally, request features feel more like accessing a user’s context. Objects are specific to a user, rather than a dependency like a database, web API, or third party service. If you are a purist, you could always register a factory within the IoC container that injects the Clock as a constructor or parameter dependency. In Startup.ConfigureServices add the following:

// for you IoC purists
services.AddScoped(provider =>
{
    var ctx = provider.GetRequiredService<IHttpContextAccessor>();
    var clock = ctx.HttpContext.Features.Get<Clock>();
    return clock ?? new Clock();
});

Another issue might stem from the mutation of our feature. You could experience unexpected behavior based on how many code blocks are mutating your data. If your request feature has connections to a data source, it may just make sense to read it every time and bypass a request feature.

Request features and the HTTP context are tied together. Request features make sense in a web application, but can be problematic if you share code between multiple hosting environments. Improper implementation can lead to leaked abstractions into unexpected areas of your system.

Finally, this one is minor but significant to mention. Ordering your middleware matters. You must instantiate a feature before you can access it. Register your middleware as early as it makes sense, or you’ll have unexpected and strange exceptions.

The Difference Between Request Features and HttpContext.Items

As an ASP.NET developer, you may also be familiar with HttpContext.Items. There are a few differences, but both can be used to accomplish the same results. Both collections are scoped within the lifetime of the request and can hold objects. As for what’s different, here is what I found.

1. Different Keys

The first difference is the IFeaturesCollection keys items by the Type of the feature.

public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>, IEnumerable
{
  bool IsReadOnly { get; }
  int Revision { get; }
  object this[Type key] { get; set; }
  TFeature Get<TFeature>();
  void Set<TFeature>(TFeature instance);
}

Whereas the HttpContext.Items use an object for both the key and the value.

public override IDictionary<object, object> Items { get; set; }

2. Constraints

Since the IFeatureCollection has Type as a key, you can only have one instance of any specific feature. The constraint is a good thing, since features usually are foundational to an app, and having multiple of the same feature probably means a misconfiguration has occurred. That said, each application is different.

3. Box Allocations

This next point may be a micro-optimization but HttpContext.Items require that you box and unbox both the key and value of the pair. These allocations can lead to additional memory overhead. With request features, you are only unboxing the value, which in this case, is the feature.

Conclusion

Request features can help you increase the performance of your web applications by limiting where and when you instantiate objects. It can also lead to more clarity in your pipeline code as you can expect the feature to be ready for use. Think about the questions mentioned above and see if it makes sense to refactor some of your code to make better use of this ASP.NET Core killer addition.