Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Exploring IStartupFilter in ASP.NET Core

$
0
0
Exploring IStartupFilter in ASP.NET Core

Note The MEAP preview of my book, ASP.NET Core in Action is now available from Manning! Use the discount code mllock to get 50% off, valid through February 13.

I was spelunking through the ASP.NET Core source code the other day, when I came across something I hadn't seen before - the IStartupFilter interface. This lives in the Hosting repository in ASP.NET Core and is generally used by a number of framework services rather than by ASP.NET Core applications themselves.

In this post, I'll take a look at what the IStartupFilter is and how it is used in the ASP.NET Core infrastructure. In the next post I'll take a look at an external middleware implementation that makes use of it.

The IStartupFilter interface

The IStartupFilter interface lives in the Microsoft.AspNetCore.Hosting.Abstractions package in the Hosting repository on GitHub. It is very simple, and implements just a single method:

namespace Microsoft.AspNetCore.Hosting  
{
    public interface IStartupFilter
    {
        Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
    }
}

The single Configure method that IStartupFilter implements takes and returns a single parameter, an Action<IApplicationBuilder>. That's a pretty generic signature for a class, and doesn't reveal a lot of intent but we'll just go with it for now.

The IApplicationBuilder is what you use to configure a middleware pipeline when building an ASP.NET Core application. For example, a simple Startup.Configure method in an MVC app might look something like the following:

public void Configure(IApplicationBuilder app)  
{
    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

In this method, you are directly provided an instance of the IApplicationBuilder, and can add middleware to it. With the IStartupFilter, you are specifying and returning an Action<IApplicationBuilder>, that is, you are provided a method for configuring an IApplicationBuilder and you must return one too.

Consider this again for a second - the IStartupFilter.Configure method accepts a method for configuring an IApplicationBuilder. In other words, the IStartupFilter.Configure accepts a method such as Startup.Configure:

Startup _startup = new Startup();  
Action<IApplicationBuilder> startupConfigure = _startup.Configure;

IStartupFilter filter1 = new StartupFilter1(); //I'll show an example filter later on  
Action<IApplicationBuilder> filter1Configure = filter1.Configure(startupConfigure)

IStartupFilter filter2 = new StartupFilter2(); //I'll show an example filter later on  
Action<IApplicationBuilder> filter2Configure = filter2.Configure(filter1Configure)  

This may or may not start seeming somewhat familiar… We are building up another pipeline; but instead of a middleware pipeline, we are building a pipeline of Configure methods. This is the purpose of the IStartupFilter, to allow creating a pipeline of Configure methods in your application.

When are IStartupFilters called?

Now we better understand the signature of IStartupFilter, we can take a look at its usage in the ASP.NET Core framework.

To see IStartupFilter in action, you can take a look at the WebHost class in the Microsoft.AspNetCore.Hosting package, in the method BuildApplication. This method is called as part of the general initialisation that takes place when you call Build on a WebHostBuilder. This typically takes place in your program.cs file, e.g.:

public class Program  
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()    
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .Build();  // this will result in a call to BuildApplication()

        host.Run(); 
    }
}

Taking a look at BuildApplication in elided form (below), you can see that this method is responsible for instantiating the middleware pipeline. The RequestDelegate it returns represents a complete pipeline, and can be called by the server (Kestrel) when a request arrives.

private RequestDelegate BuildApplication()  
{
    //some additional setup not shown
    IApplicationBuilder builder = builderFactory.CreateBuilder(Server.Features);
    builder.ApplicationServices = _applicationServices;

    var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
    Action<IApplicationBuilder> configure = _startup.Configure;
    foreach (var filter in startupFilters.Reverse())
    {
        configure = filter.Configure(configure);
    }

    configure(builder);

    return builder.Build();

First, this method creates an instance of an IApplicationBuilder, which will be used to build the middleware pipeline, and sets the ApplicationServices to a configured DI container.

The next block is the interesting part. First, an IEnumerable<IStartupFilter> is fetched from the DI container. As I've already hinted, we can configure multiple IStartupFilters to form a pipeline, so this method just fetches them all from the container. Also, the Startup.Configure method is captured into a local variable, configure. This is the Configure method that you typically write in your Startup class to configure your middleware pipeline.

Now we create the pipeline of Configure methods by looping through each IStartupFilter (in reverse order), passing in the Startup.Configure method, and then updating the local variable. This has the effect of creating a nested pipeline of Configure methods. For example, if we have three instances of IStartupFilter, you will end up with something a little like this, where the the inner configure methods are passed in the parameter to the outer methods:

Exploring IStartupFilter in ASP.NET Core

The final value of configure is then used to perform the actual middleware pipeline configuration by invoking it with the prepared IApplicationBuilder. Calling builder.Build() generates the RequestDelegate required for handling HTTP requests.

What does an implementation look like?

We've described in general what IStartupFilter is for, but it's always easier to have a concrete implementation to look at. By default, the WebHostBuilder registers a single IStartupFilter when it initialises - the AutoRequestServicesStartupFilter:

public class AutoRequestServicesStartupFilter : IStartupFilter  
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            builder.UseMiddleware<RequestServicesContainerMiddleware>();
            next(builder);
        };
    }
}

Hopefully, the behaviour of this class is fairly obvious. Essentially it adds an additional piece of middleware, the RequestServicesContainerMiddleware, at the start of your middleware pipeline.

This is the only IStartupFilter registered by default, and so in that case the parameter next will be the Configure method of your Startup class.

And that is essentially all there is to IStartupFilter - it is a way to add additional middleware (or other configuration) at the beginning or end of the configured pipeline.

How are they registered?

Registering an IStartupFilter is simple, just register it in your ConfigureServices call as usual. The AutoRequestServicesStartupFilter is registered by default in the WebHostBuilder as part of its initialisation:

private IServiceCollection BuildHostingServices()  
{
    ...
    services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
    ...
}

The RequestServicesContainerMiddleware

On a slightly tangential point, but just for interest, the RequestServicesContainerMiddleware (that is registered by the AutoRequestServicesStartupFilter) is shown in reduced format below:

public class RequestServicesContainerMiddleware  
{
    private readonly RequestDelegate _next;
    private IServiceScopeFactory _scopeFactory;

    public RequestServicesContainerMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var existingFeature = httpContext.Features.Get<IServiceProvidersFeature>();

        // All done if request services is set
        if (existingFeature?.RequestServices != null)
        {
            await _next.Invoke(httpContext);
            return;
        }

        using (var feature = new RequestServicesFeature(_scopeFactory))
        {
            try
            {
                httpContext.Features.Set<IServiceProvidersFeature>(feature);
                await _next.Invoke(httpContext);
            }
            finally
            {
                httpContext.Features.Set(existingFeature);
            }
        }
    }
}

This middleware is responsible for setting the IServiceProvidersFeature. When created, the RequestServicesFeature creates a new IServiceScope and IServiceProvider for the request. This handles the creation and disposing of dependencies added to the dependency injection controller with a Scoped lifecycle.

Hopefully it's clear why it's important that this middleware is added at the beginning of the pipeline - subsequent middleware may need access to the scoped services it manages.

By using an IStartupFilter, the framework can be sure the middleware is added at the start of the pipeline, doing it an extensible, self contained way.

When should you use it?

Generally speaking, I would not imagine that there will much need for IStartupFilter to be used in user's applications. By their nature, users can define the middleware pipeline as they like in the Configure method, so IStartupFilter is rather unnecessary.

I can see a couple of situations in which IStartupFilter would be useful to implement:

  1. You are a library author, and you need to ensure your middleware runs at the beginning (or end) of the middleware pipeline.
  2. You are using a library which makes use of the IStartupFilter and you need to make sure your middleware runs before its does.

Considering the first point, you may have some middleware that absolutely needs to run at a particular point in the middleware pipeline. This is effectively the use case for the RequestServicesContainerMiddleware shown previously.

Currently, the order in which services T are registered with the DI container controls the order they will be returned when you fetch an IEnumerable<T> using GetServices(). As the AutoRequestServicesStartupFilter is added first, it will be returned first when fetched as part of an IEnumerable<IStartupFilter>. Thanks to the call to Reverse() in the WebHost.BuildApplication() method, its Configure method will be the last one called, and hence the outermost method.

If you register additional IStartupFilters in your ConfigureServices method, they will be run prior to the AutoRequestServicesStartupFilter, in the reverse order that you register them. The earlier they are registered with the container, the closer to the beginning of the pipeline any middleware they define will be.

This means you can control the order of middleware added by IStartupFilters in your application. If you use a library that registers an IStartupFilter in its 'Add' method, you can choose whether your own IStartupFilter should run before or after it by whether it is registered before or after in your ConfigureServices method.

The whole concept of IStartupFilters is a little confusing and somewhat esoteric, but it's nice to know it's there as an option should it be required!

Summary

In this post I discussed the IStartupFilter and its use by the WebHost when building a middleware pipeline. In the next post I'll explore a specific usage of the IStartupFilter.


Viewing all articles
Browse latest Browse all 743

Trending Articles