The ASP.NET Core technology stack works on the pipeline concept. A user request travels through the pipeline, where you have opportunities to handle the request in various forms. The approach to enhancing the pipeline in ASP.NET Core has been a mixture of Middleware and paradigm-specific filters. Filters for ASP.NET Core MVC give you MVC-specific idioms to work with, while Middleware typically works with the rawest elements of an HttpContext.

With Minimal APIs, you can implement the IEndpointFilter interface I wrote about previously.

In this post, we’ll see a technique to apply the IEndpointFilter to all endpoints, similar to ASP.NET Core MVC’s global filters. As a bonus, it’s pretty straightforward.

Why not Middleware?

Middleware in ASP.NET Core is designed to operate as a gate to incoming HTTP requests and outgoing HTTP responses. You could use the IMiddleware interface to process the request, but you’ll quickly find out you’re working with lower-level intrinsics than you might want. Let’s take a look at an implementation of a new Middleware.

public class MyMiddleware: IMiddleware  
{  
    public Task InvokeAsync(HttpContext context, RequestDelegate next)  
    {        
        throw new NotImplementedException();  
    }
}

You’ll quickly notice the use of HttpContext and the RequestDelegate types. These types can give you access to elements of the request pipeline you might need, but the accessing code can be verbose and opaque. In other words, you’ll quickly find your Middleware code exploding in complexity.

Middleware is an excellent tool for applying similar functionality across all ASP.NET Core development paradigms, including MVC, Razor Pages, Minimal APIs, and Blazor. To offer that breadth, middleware implementations must work at the lowest abstractions.

On the other hand, the IEndpointFilter is explicitly designed to work with the latest concept of Endpoints, which includes Minimal APIs, MVC, and Razor Pages. Endpoints generally focus on inputs and outputs, with the pipeline responsible for executing those results. It’s easier to see this in the implementation of a filter.

public class ScreamingFilter: IEndpointFilter  
{  
    public async ValueTask<object?> InvokeAsync(  
        EndpointFilterInvocationContext context,  
        EndpointFilterDelegate next)  
    {        
        var result = await next(context);  
        return result is string s   
            ? $"{s}!!!!"   
            : result;  
    }
}

Rather than working with HttpContext, we are working with the return value of our endpoint. That allows us to deal with objects and their properties. This opens up a world of possibilities regarding object inspection and enrichment that can be difficult to do with middleware.

OK, enough theory. Let’s get to Global Endpoint Filters for Minimal APIs.

The One Trick That Makes It Possible

First of all, and I’ll be the first to admit it, this technique can seem “simple” on the surface, but it works surprisingly well.

You need a global’ Map’ group to get Global Endpoint Filters for Minimal APIs working. Let’s see this in action.

var builder = WebApplication.CreateBuilder(args);  
  
var app = builder.Build();  

// the magic 🪄
var global = app  
    .MapGroup(string.Empty)  
    .AddEndpointFilter<ScreamingFilter>();  
  
global.MapGet("/", () => "Hello World");  
global.MapGet("/hi", () => "Hi");  
global.MapGroup("/what").MapGet("/now", () => "🤷");  
  
app.Run();  
  
public class ScreamingFilter: IEndpointFilter  
{  
    public async ValueTask<object?> InvokeAsync(  
        EndpointFilterInvocationContext context,  
        EndpointFilterDelegate next)  
    {        
        var result = await next(context);  
        return result is string s   
            ? $"{s}!!!!"   
            : result;  
    }
}

The trick is to register all endpoints off of the root MapGroup. Each additional endpoint will now inherit the filters of the group. Since the global group has no route prefix, it doesn’t affect any paths of registered endpoints. The explicit type of global is RouteGroupBuilder, which implements IEndpointRouteBuilder and IEndpointConventionBuilder, which should give you access to the same functionality as registering a regular endpoint, including having additional sub-groups.

Now, you have what are essentially global endpoint filters for all your minimal API endpoints. Pretty cool! Note that endpoint execution ordering will still apply. All “global” endpoint filters will execute first, then any filters applied directly to the endpoints.

As always, thanks for reading my blog posts and for sharing my posts with friends and colleagues. Cheers :)