Inserting middleware between UseRouting() and UseEndpoints() as a library author - Part 1
Andrew Lock "Sock"
Posted on February 1, 2020
This post is in response to a question from a reader about how library authors can ensure consumers of their library insert the library's middleware at the right point in the app's middleware pipeline. Unfortunately there's not really a great solution to that (with a couple of exceptions), but this post highlights one possible approach.
Introduction: ordering is important for middleware
One of the big changes I discussed in my recent series on ASP.NET Core 3.0 is that routing now uses Endpoint Routing by default. This manifests most visibly in your ASP.NET Core apps as two separate calls in the Startup.Configure
method that configures the middleware pipeline for your app:
public void Configure(IApplicationBuilder app)
{
// ... middleware
app.UseRouting();
// ... other middleware
app.UseEndpoints(endpoints =>
{
// ... endpoint configuration
});
}
As shown above, there are two separate calls related to routing in a typical .NET Core 3.0 middleware pipeline: UseRouting()
and UseEndpoints()
. In addition, middleware can be be placed before, or between these calls. One thing that often trips people up in general is that where you place your middleware is very important. It's important you understand what each piece of middleware is doing so that you can put it in the right point in the pipeline.
For example, in ASP.NET Core 3.0 it's important that you place the AuthenticationMiddleware
and AuthorizationMiddleware
between the two routing middleware calls (and that you place authentication middleware before the authorization middleware):
public void Configure(IApplicationBuilder app)
{
// ... middleware
app.UseRouting();
app.UseAuthentication(); // Must be after UseRouting()
app.UseAuthorization(); // Must be after UseAuthentication()
app.UseEndpoints(endpoints =>
{
// ... endpoint configuration
});
}
This requirement is mentioned in the documentation, but it's not very obvious. For example, an issue Rick Strahl ran into when upgrading his Album Viewer sample to .NET Core 3.0 was related to exactly this - middleware added in the wrong order.
ASP.NET Core now does some checks to try and warn you at runtime when you have configured your pipeline incorrectly, as in the case described above. However it only catches a couple of cases, so you still need to be careful when building your pipeline.
This leads us to the heart of the question I received - if you're a library author, how can you ensure your middleware is added at the correct point in a consumer's middleware pipeline?
Using IStartupFilter
The simplest scenario is where you need to add middleware to a user's app and you can add it near the start of the middleware pipeline. For this use-case, there's IStartupFilter
. I discussed IStartupFilter
in a previous post (over 3 years ago now, doesn't time fly!) but nothing has really changed about it since then, so I'll only describe it briefly here - check out that previous post for a more detailed explanation.
IStartupFilter
provides a mechanism for adding middleware to an app's pipeline by adding a service to the DI container. When building an app's middleware pipeline, the ASP.NET Core infrastructure looks for any registered IStartupFilter
s and runs them, essentially providing a mechanism for tacking middleware onto the beginning of an app's pipeline.
For example, the ForwardedHeadersStartupFilter
automatically adds the ForwardedHeadersMiddleware
to the start of a middleware pipeline. This IStartupFilter
is (conditionally) added to the DI container by the WebHost
on app startup, so you don't have to remember explicitly add it as part of your app's middleware configuration.
This approach is very useful in many cases, but has one significant limitation - you can only add middleware at the start (or end) of the pipeline defined in Startup.Configure
. There's no simple way to add middleware in the middle.
Unfortunately, that requirement has likely become more common in ASP.NET Core 3.0 with endpoint routing and the separate UseRouting()
and UseEndpoints()
calls. IStartupFilter
doesn't provide an easy mechanism for adding middleware at an arbitrary location in the pipeline, so you have to get inventive.
Taking a lead from the AnalysisMiddleware
After initially dismissing the task as impossible, I had a small flashback to some posts I wrote about the Microsoft.AspNetCore.MiddlewareAnalysis package. This package can be used to log all the middleware that is executed as part of the middleware pipeline.
I explored how to use the package in a previous post, and looked at how it was implemented in another. Given we're going to use a similar approach to insert middleware between the calls to UseRouting()
and UseEndpoints()
, I'll give an overview of the approach here. For a more detailed understanding, check out my previous posts.
The MiddlewareAnalysis package uses an IStartupFilter
to hook into the middleware configuration process of an app. But instead of just adding middleware to the start (or end) of the pipeline, it wraps the IApplicationBuilder
instance in a new type, the AnalysisBuilder
:
public class AnalysisStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
var wrappedBuilder = new AnalysisBuilder(builder);
next(wrappedBuilder);
// There's a couple of other bits here I'll gloss over for now
};
}
}
The AnalysisBuilder
implements IApplicationBuilder
, and its purpose is to intercept any calls to Use()
that add middleware to the pipeline. If you follow the method calls far enough down, all calls to IApplicationBuilder
that modify the pipeline call Use()
, whether it's UseStaticFiles()
, UseAuthentication()
, or UseMiddleware<MyCustomMiddleware>()
.
When the app calls Use
on the AnalysisBuilder
, the builder adds it to the pipeline as normal, but it first adds an extra piece of middleware, the AnalysisMiddleware
:
public class AnalysisBuilder : IApplicationBuilder
{
private IApplicationBuilder InnerBuilder { get; }
public AnalysisBuilder(IApplicationBuilder inner)
{
InnerBuilder = inner;
}
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
return InnerBuilder
.UseMiddleware<AnalysisMiddleware>()
.Use(middleware);
}
}
The end result is that an instance of AnalysisMiddleware
is interleaved between all the other middleware in your pipeline:
The AnalysisMiddleware
itself determines the name of the next middleware in the pipeline in its constructor by interrogating the provided RequestDelegate
:
public class AnalysisMiddleware
{
private readonly string _middlewareName
public AnalysisMiddleware(RequestDelegate next)
{
_middlewareName = next.Target.GetType().FullName;
}
// ...
}
After looking through the code, I gleaned a few key points:
- We can create an
IStartupFilter
/IApplicationBuilder
that can "bookend" each middleware added with an extra piece of middleware - The
Type
of the next middleware in the pipeline can be retrieved when the middleware is processing, but not when it's being constructed. i.e. you can get the name of the next middleware in the pipeline fromAnalysisMiddleware
, but not from theAnalysisBuilder
. - You can't easily get the name of the previous middleware in the pipeline.
With this in mind, I set about finding a solution to the original problem, inserting middleware between the UseRouting()
and UseEndpoints()
from a class library.
Inserting middleware before UseEndpoints
Given the final point raised in the previous section - it's not possible to check which middleware executed previously to the current one, I decided the easiest location to insert middleware would be just before the UseEndpoints()
call which adds the EndpointMiddleware
to the pipeline.
The overall approach is very similar to that used in the MiddlewareAnalysis package. For this example I named the middleware ConditionalMiddleware
, as it only runs under a single condition - when the next middleware is of a given type:
- Create an
IStartupFilter
that replaces the defaultIApplicationBuilder
with a custom one,ConditionalMiddlewareBuilder
, that intercepts calls toUse(...)
- Every time middleware is added to the pipeline, add an instance of the
ConditionalMiddleware
first. - When the
ConditionalMiddleware
executes, check if the next middleware is the one we're looking for. If it is, run the additional logic before invoking the next middleware in the pipeline.
I'll start with the easy bit, the ConditionalMiddleware
itself. For this example the "extra logic" we're going to execute is just to write out a log message. In practice you might use it to set a request feature or do some sort of request-specific check.
internal class ConditionalMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ConditionalMiddleware> _logger;
private readonly string _runBefore;
private readonly bool _runMiddleware;
public ConditionalMiddleware(RequestDelegate next, ILogger<ConditionalMiddleware> logger, string runBefore)
{
// Check if the next middleware is of the required type
_runMiddleware = next.Target.GetType().FullName == runBefore;
_next = next;
_logger = logger;
_runBefore = runBefore;
}
public async Task Invoke(HttpContext httpContext)
{
// if the next middleware is the required type, run the exta logic
if (_runMiddleware)
{
_logger.LogInformation("Running conditional middleware before {NextMiddleware}", _runBefore);
}
// either way, call the next middleware in the pipeline
await _next(httpContext);
}
}
In this example I'm passing in the name of the middleware that we want to run our custom logic before (i.e."EndpointMiddleware"
for my original example). We then check whether the next middleware is the one we're looking for. We can do the check in the constructor, as the middleware pipeline is fixed after it's built - the check ensures that the additional functionality is only run where we need it to be:
Next up is the ConditionalMiddlewareBuilder
. This is the wrapper class that we use to inject our ConditionalMiddleware
between each "real" middleware in the pipeline. It's mostly just a wrapper around the InnerBuilder
provided in the constructor:
internal class ConditionalMiddlewareBuilder : IApplicationBuilder
{
// The middleware we're looking for is provided as a constructor argument
private readonly string _runBefore;
public ConditionalMiddlewareBuilder(IApplicationBuilder inner, string runBefore)
{
_runBefore = runBefore;
InnerBuilder = inner;
}
private IApplicationBuilder InnerBuilder { get; }
public IServiceProvider ApplicationServices
{
get => InnerBuilder.ApplicationServices;
set => InnerBuilder.ApplicationServices = value;
}
public IDictionary<string, object> Properties => InnerBuilder.Properties;
public IFeatureCollection ServerFeatures => InnerBuilder.ServerFeatures;
public RequestDelegate Build() => InnerBuilder.Build();
public IApplicationBuilder New() => throw new NotImplementedException();
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
// Add the conditional middleware before each other middleware
return InnerBuilder
.UseMiddleware<ConditionalMiddleware>(_runBefore)
.Use(middleware);
}
}
The ConditionalMiddlewareBuilder
is added to the application using an IStartupFilter
that wraps the "original" IApplicationBuilder
with our imposter:
public class ConditionalMiddlewareStartupFilter : IStartupFilter
{
private readonly string _runBefore;
public ConditionalMiddlewareStartupFilter(string runBefore)
{
_runBefore = runBefore;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
// wrap the builder with our interceptor
var wrappedBuilder = new ConditionalMiddlewareBuilder(builder, _runBefore);
// build the rest of the pipeline using our wrapped builder
next(wrappedBuilder);
};
}
}
Finally, lets create a couple of extension methods to make adding our new middleware easy:
public static class ConditionalMiddlewareExtensions
{
// Add ConditionalMiddlware that runs just before the middleware given by "beforeMiddleware"
public static IServiceCollection AddConditionalMiddleware(this IServiceCollection services, string beforeMiddleware)
{
// Add the startup filter to wrap the middleware
return services.AddTransient<IStartupFilter>(_ => new ConditionalMiddlewareStartupFilter(beforeMiddleware));
}
// A helper that runs the conditional middleware just before the call to `UseEndpoints()`
public static IServiceCollection AddConditionalMiddlewareBeforeEndpoints(this IServiceCollection services)
{
return services.AddConditionalMiddleware("Microsoft.AspNetCore.Routing.EndpointMiddleware");
}
}
With all that configured, the one thing that remains is to add the middleware to our app:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddConditionalMiddlewareBeforeEndpoints(); // <-- Add this line
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// <-- The ConditionalMiddleware will execute here
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
As you can see, we only had to add one line. This is great for library authors, as they don't have to deal with issues arising from users putting the middleware in the wrong place. And it's great for consumers because they don't have to worry about getting it wrong either! Just add the services to your DI container and you're good to go.
Of course, this example is just a proof of concept - it doesn't do anything interesting other than print the following log message just before the EndpointMiddleware
, but it demonstrates an interesting technique:
info: MyTestApp.ConditionalMiddleware[0]
Running conditional middleware before Microsoft.AspNetCore.Routing.EndpointMiddleware
The other good thing is that while this looks complicated, and it adds a lot of extra middleware, the impact at runtime should be minimal. The "next middleware check" is only executed once per middleware instance, and when the evaluation returns false
the runtime effect will be very small - a single additional if
check per middleware. Still, I haven't found myself needing to do something like this before, so I'd be interested to hear if someone tries it and how they get on!
Summary
In this post I discussed the problem of a library author trying to insert middleware at a precise point in a consuming app's pipeline. You can use IStartupFilter
to insert middleware at the start of the pipeline, but it doesn't allow you to insert middleware at an arbitrary location, such as between the UseRouting()
and UseEndpoints()
calls.
As a potential solution to the problem, I gave a brief overview of the MiddlewareAnalysis package that I've discussed previously, which inserts AnalysisMiddleware
between every other middleware in your pipeline.
I then described a similar approach that can be used to insert our ConditionalMiddleware
between each middleware in the pipeline. The ConditionalMiddleware
can access the name of the next middleware in the pipeline, so that only the middleware instance placed just before the target middleware (EndpointMiddleware
) executes its logic. The end result is somewhat convoluted, but achieves the desired goals!
Posted on February 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 1, 2020
February 9, 2020