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 IStartupFilter
s 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 IStartupFilter
s 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:
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:
- You are a library author, and you need to ensure your middleware runs at the beginning (or end) of the middleware pipeline.
- 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 IStartupFilter
s 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
.