Applying C# middleware conditionally

An investigation into how middleware can be filtered to only apply for some endpoints in ASP.NET

#.Net#Middleware

Like any reasonably mature web framework, ASP.NET supports middleware for HTTP endpoints.

The general concept is very simple. You have a chain of functions that is invoked before the actual handler for the endpoint. These functions can modify the request or stop it altogether. A basic, yet nonsensical example for middleware that lets 50% of requests fail could look like this.

app.Use(async (context, next) =>
{
    var chance = Random.Shared.NextDouble();
    if(chance < 0.5)
    {
        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("No luck");
        return;
    }

    await next.Invoke(context);
});

This middleware would apply to all endpoints of the application.


For a recent project I needed a middleware that would only apply to certain endpoints. This is not an uncommon use case. Think of the .RequireAuthorization(); method, which can be used to require authorization for a single endpoint or a group of endpoints.

So how can we achieve this filtering?

Endpoint Metadata

When mapping a new endpoint, a whole slew of things happens. One of which is that you create an endpoint builder from which the final endpoint is then built. This builder contains information such as the ServiceProvider associated with the endpoint, the display name, and metadata, along with some others.

For us, the metadata is the important part. It is just a plain list of objects.

public System.Collections.Generic.IList<object> Metadata { get; }

This gives us the power to add very powerful custom objects to an endpoint. For this case we only needed the information of whether the middleware should activate for a given endpoint. So all we do is just add a marker class.

public sealed class RequireInitializationMetadata
{
}

To make adding this class to an endpoint easier, we can add an extension method

public static class InitializationEndpointConventionBuilderExtensions
{
    public static TBuilder RequireInitialization<TBuilder>(this TBuilder builder)
        where TBuilder : IEndpointConventionBuilder
    {
        builder.Add(endpointBuilder => { endpointBuilder.Metadata.Add(new RequireInitializationMetadata()); });

        return builder;
    }
}

To apply this metadata, we simply run it on an endpoint or endpointgroup

var group = app.MapGroup("api/missions").RequireInitialization().

Filtering in the middleware

The middleware I needed for my project was to handle an initialization phase of the application during which some requests would not be handleable. For clients, I wanted to expose detailed information regarding the readiness.

For that, I added this middleware.

app.Use(async (context, next) =>
{
    var metaData = context.GetEndpoint()?.Metadata.GetMetadata<RequireInitializationMetadata>();
    // No initialization required. Just call endpoint as usual
    if (metaData is null)
    {
        await next.Invoke(context);
        return;
    }

    var initManager = context.RequestServices.GetRequiredService<IInitializationInfo>();
    if (!initManager.Completed)
    {
        context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsync(JsonSerializer.Serialize(initManager.InitializationResources));
        return;
    }

    await next.Invoke(context);
});

Here we first check if the endpoint contains the marker class from before. If it doesn’t, our middleware simply passes on the request and does nothing.

Otherwise, the initialization manager is checked for completion. When not yet completed, we don’t want to invoke the endpoint but rather return that the service is not yet ready for request (503), together with the information about the initialization resources.


With this, the middleware only lets requests pass when the app is actually ready for them.