When registering services in ASP.Net Core, often you have to register
the services in Startup.ConfigureServices
and then tell the
IApplicationBuilder
to actually use them in Startup.Configure
.
Take the example of JwtBearer Authentication. You have to remember to call
services.AddAuthentication(options => ...)
in ConfigureServices
and
app.UseAuthentication()
in Configure
.
Not only is this open to making mistakes, it scatters the configuration of a service across multiple methods. This is not particularly convenient if you want to distribute an API because consumers of your API don’t have one place to register and configure your service.
Enter the IStartupFilter
, part of the Microsoft.AspNetCore.Hosting.Abstractions
package. It defines one method:
Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
Note that this does not apply to the Generic Host, which has a different startup model.
When you call Build
on your HostBuilder
(either Generic or Web) the runtime retrieves an IEnumerable
of IStartupFilters
from its
ServiceCollection
and then iterates through them in order invoking the Configure
method on your app’s ApplicationBuilder
.
As a particularly contrived version, consider a middleware that has its own dependencies.
public class DemoMiddleware
{
readonly RequestDelegate _next;
readonly IMessageWriter _messageWriter;
public DemoMiddleware(RequestDelegate next, IMessageWriter messageWriter)
{
_next = next;
_messageWriter = messageWriter;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync(_messageWriter.GetMessage());
await _next(context);
}
}
This middleware simply injects a message from the IMessageWriter directly into the response stream.
In order to register this, our users need to add
services.AddSingleton<IMessageWriter, MessageWriter>();
to their ConfigureServices
and
app.UseMiddleware<DemoMiddleware>();
to their Configure
.
To make this easier to use for our users, we can leverage the IStartupFilter
and
a convenient extension method.
public static class DemoMiddlewareRegistrationExtensions
{
public static void AddDemoMiddleware(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IMessageWriter, MessageWriter>();
services.AddTransient<IStartupFilter, DemoMiddlewareStartupFilter>();
}
}
Here we’re adding the IMessageWriter service and an IStartupFilter service. The IStartupFilter service looks like this:
public class DemoMiddlewareStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) =>
app =>
{
app.UseMiddleware<DemoMiddleware>();
next(app);
};
}
Here we return an Action
on IApplicationBuilder
which is where we tell AspNetCore
to use our Middleware, remembering to call the passed in next
afterwards.
(Of course depending on our needs we may be terminating the call here, e.g. if we can’t start up in a
consistent state we may not wish to continue starting up.)
Now all our users need do is add one line to call this extension method from the ConfigureServices
method of Startup.cs
:
services.AddDemoMiddleware(Configuration);
Note that I have passed the configuration through to the extension method purely for illustrative purposes here. It’s not used in this example and can be omitted if you don’t need it in your code.
Now this is a contrived example, but you can see how we can use this approach to add sophisticated setup logic to our APIs with a minimal amount of setup for the consumers of those APIs.
There is still a minor complaint with the approach as it stands. Our API users will expect
that only Service Registration occurs in the ConfigureServices
method, but we’re also
telling our Application to use this Middleware. That’s a bit of a violation of the
Principle of Least Astonishment.
In general if we’re going to add a whole suite of services and functionality, I think the
best place for that is on the HostBuilder
.
If we add a new extension method to our DemoMiddlewareRegistrationExtensions
as follows:
public static IWebHostBuilder UseDemoMiddleware(this IWebHostBuilder hostBuilder)
{
hostBuilder.ConfigureServices((context, builder) => builder.AddDemoMiddleware(context.Configuration));
return hostBuilder;
}
We can now simply amend our CreateWebHostBuilder
method as follows:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseDemoMiddleware();
Note that the GenericHost does not have the same Startup
pattern as ASP.Net Core (Instead it registers IHostedService
instances)
so the IStartupFilter
can be registered, but will never be called.
In conclusion, IStartupFilter
allows you to create a single point for consumers of your API to both
register and configure your services.
csharp
dotnetcore
asp
]