Route handler filters in .NET 7

Learning about minimal api route handler filters coming in .NET 7

Home DailyDrop

Daily Knowledge Drop

Coming with .NET 7 (most likely, .NET 7 is still in preview so its not 100% guaranteed to be included), is the IRouteHandlerFilter interface, which allows for intercepting requests/response to and from a specific minimal endpoint.

This enabled cross-cutting concerns to be coded once, and then applied to the relevent endpoints. They operate similar to the .NET middleware but are applied at a specific endpoint level, and not a level higher targeting all routes.


IRouteHandlerFilter

Definition

First, lets define the IRouteHandlerFilter implementation to later be applied to a minimal api endpoint. In the below example, a router handler filter is defined to measure how long a call to the endpoint takes:

public class RouteLogger : IRouteHandlerFilter
{
    public async ValueTask<object?> InvokeAsync(RouteHandlerInvocationContext context, 
        RouteHandlerFilterDelegate next)
    {
        // record the time before the endpoint (or next filter is called)
        Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
            $"RouteLogger - before endpoint called");

        var result = await next.Invoke(context);

        // record the time after the endpoint (or next filter is called)
        Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
            $"RouteLogger - after endpoint called");

        return result;
    }
}

The IRouteHandlerFilter interface only has one method to implement - InvokeAsync, which takes two parameters:

  • RouteHandlerInvocationContext: this contains a reference to the HttpContext, as well as a list of Arguments which can be modified to be passed between filters
  • RouteHandlerFilterDelegate: this contains a delegate to the next IRouteHandlerFilter implementation if multiple have been applied, otherwise it will route to the actual endpoint handler

In the above sample, a message is logged when the method is entered, the next delegate is invoked, and then a message logged just before the return. If next is not invoked, the pipeline to the endpoint is short-circuited and the endpoint handler will never be invoked. This allows checks or validation to be performed (authentication checks for example), and short-circuit if the checks fail.

Application

Applying the filter to the endpoint is very simple:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/endpoint", () =>
{
    Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
        $"Endpoint handler");

    return "Endpoint has been called";
}).AddFilter<RouteLogger>(); // add the filter

app.Run();

On the endpoint definition, the AddFilter method is called, with the IRouteHandlerFilter implementation specified. Multiple implementations can be linked together to form a pipeline to the endpoint handler.

The above was written using .NET 7 Preview 5. AddFilter() has been renamed to AddRouteHandlerFilter() in Preview 6 and will be renamed again to AddEndpointFilter() starting in Preview 7.


Middleware

As mentioned in the introduction, route handler filters act similar to the middleware. Where route handler filters are applied to specific endpoints, middleware is applied to requests coming in on any route.

Below a middleware function is defined, which also performs logging:

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

// define the middleware
// The Func defined is called on every 
// request to any endpoints
app.Use(async (context, next) =>
{
    Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
        $"Middleware - before endpoint called");

    await next(context);

    Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
        $"Middleware - after endpoint called");
});

app.MapGet("/endpoint", () =>
{
    Console.WriteLine($"{DateTime.Now:MM/dd/yyyy hh:mm:ss.fff}: " +
        $"Endpoint handler");

    return "Endpoint has been called";
}).AddFilter<RouteLogger>();

app.Run();

The middleware has a similar structure to a route handler filter, with an HttpContext and a RequestDelegate as arguments. The RequestDelegate argument next is also invoked, to call the next middleware component, or the endpoint handler. If not called, short-circuiting will occur, just as with the route handler filter.

In the above example, the middleware was defined as a lambda function while the filter was defined as a concrete IRouteHandlerFilter implementation - either of these can also be defined the either way. Middleware could be defined as a concrete implementation, and a route handler filter could be defined as a lambda function.
If there are a number of middleware or route handler filter components being used, it's usually better to use the concrete implementation method to keep the startup code cleaner, and keep all pipeline logic in one place (their own folder, for example).

Executing the above code and browsing to the /endpoint, results in the following output:

07/27/2022 08:42:27.851: Middleware - before endpoint called
07/27/2022 08:42:27.859: RouteLogger - before endpoint called
07/27/2022 08:42:27.860: Endpoint handler
07/27/2022 08:42:27.860: RouteLogger - after endpoint called
07/27/2022 08:42:27.865: Middleware - after endpoint called

As expected, the middleware is called first, before the http request is passed onto the endpoint specific route handler filter(s) (if any are defined), before the actual endpoint handler is called.


Notes

As mentioned, middleware is applied to all endpoints, so if some middleware logic is only applicable to a certain endpoint(s), then currently filter logic needs to be specific in the middleware to determine if the middleware functionality should be applied to the request or not. Having the ability to granularly apply route handler filter "middleware" on specific endpoint(s) allows for greater flexibility and is a welcome addition which brings functionality closer to being on par with that of MVC (which has ActionFilter)


References

Minimal API Route Handler Filters


Daily Drop 145: 24-08-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net minimalapi .net7 routehandler filter