DEV Community

Andrew Lock "Sock"
Andrew Lock "Sock"

Posted on • Originally published at andrewlock.net on

Inserting middleware between UseRouting() and UseEndpoints() as a library author - Part 1

Inserting middleware between UseRouting() and UseEndpoints() as a library author - Part 1

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 IStartupFilters 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:

A middleware pipeline where the AnalysisMiddleware is added before every other middleware

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 from AnalysisMiddleware, but not from the AnalysisBuilder.
  • 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 default IApplicationBuilder with a custom one, ConditionalMiddlewareBuilder, that intercepts calls to Use(...)
  • 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:



Conditional middleware is added between all the other middleware, but only executes its logic in one location

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!

Top comments (0)