While never sold as an outright replacement for ASP.NET Core MVC, ASP.NET Core Minimal APIs are bound to have developers asking, “Which one should I use?” The answer depends on your use case, but in reality, you can use all the methodologies found in ASP.NET Core together. While you can create a tasty cocktail of ASP.NET Core approaches in your application, you’ll likely repeat a lot of pipeline-based functionalities. With ASP.NET Core MVC, you have a decade-long library of ActionFilter implementations to pull from, allowing you to fine-tune a request processing pipeline. With ASP.NET Core Minimal APIs, you have to do everything in every endpoint. Now with the release of .NET 7, which introduces the IEndpointFilter.

In this post, we’ll see how to write several kinds of IEndpointFilter implementations and how they can help reduce the repetition in your Minimal API apps.

What’s An IEndpointFilter?

An IEndpointFilter allows you to refactor cross-cutting functionality into a single class with the ability to access the incoming user request. You can modify the request, change the response, or short-circuit the request pipeline immediately. Common cross-cutting functionality that may be a candidate for an IEndpointFilter might be input validation, authorization, context hydration with additional info, and response transformation, to name a few. If you are familiar with ASP.NET Core MVC, you can think of them as similar to the IActionFilter interface but more akin to middleware isolated to an endpoint.

The interface of IEndpointFilter has one method to implement. Still, you have access to the HttpContext through the EndpointFilterInvocationContext, and the subsequent EndpointFilterDelegate in the request pipeline provides you with all the ingredients you need to do what you wish.

namespace Microsoft.AspNetCore.Http;

public interface IEndpointFilter
{
    ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next);
}

You need to invoke the AddEndpointFilter method on your endpoint definition to register a route filter.

app.MapGet("/before/{name?}", (string? name) => 
        new MyResult(name ?? "Hi!"))
    .AddEndpointFilter<BeforeEndpointExecution>();

Let’s get into some examples of IEndpointFilter implementations and how you can handle requests before your endpoint executes, how to short circuit execution, and how to modify a result before returning the response.

Filter Before Endpoint Executions

There are many reasons you typically want to run code before your endpoint executes; the most practical reason I can think of is validating user input. Unfortunately, unlike ASP.NET Core MVC with ModelState, Minimal APIs currently lack a first-class validation approach, which means you need to provide your method. A filter might be a great way to introduce input validation. Certainly, filters are code, and you can do anything you wish. Let’s look at a basic IEndpointFilter implementation that runs before an endpoint executes.

public class BeforeEndpointExecution : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next
    )
    {
        if (context.HttpContext.GetRouteValue("name") is string name)
        {
            return Results.Ok(new MyResult($"Hi {name}, this is from the filter!"));
        }

        return await next(context);
    }
}

The placement of the await next(context) is critical here. The code runs before any other element in the chain, including the endpoint itself. It checks if the route has a path element of name in this sample. If our route includes the value, we want to return a different result. You want to return an IResult, which the ASP.NET Core pipeline can process. If the route value is missing, ASP.NET Core will execute our endpoint.

In this filter, we also have the opportunity to inject values into the current HttpContext. Keep in mind that any values you add will only be accessible in your endpoint if you have injected the HttpContext instance into your endpoint parameters.

(HttpContext ctx) => { /* my code goes here */ }

Now, let’s register our filter with an endpoint.

// run filter before the endpoint executes
app.MapGet("/before/{name?}", (string? name) => 
        new MyResult(name ?? "Hi!"))
    .AddEndpointFilter<BeforeEndpointExecution>();

Note that the endpoint will only execute its function body if the parameter name is excluded from the route path.

Short Circuit Endpoint Executions

Like running code before the endpoint executes, you can short-circuit the entire request pipeline. Let’s look at the most straightforward example.

public class ShortCircuit: IEndpointFilter
{
    public ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context, 
        EndpointFilterDelegate next)
    {
        // because YOLO!
        return new ValueTask<object?>(Results.Json(new { Fizz = "Buzz" }));
    }
}

Note that we’re missing the call to the next EndpointFilterDelegate. We exclude the invocation because we have no intention of continuing down the chain. Let’s see it registered on our endpoint.

// run the filter before the endpoint ever executes
app.MapGet("/short-circuit", () => "It doesn't matter")
    .AddEndpointFilter<ShortCircuit>();

The short circuit seems like the strangest filter to implement, but I could see a use case for disabling an endpoint based on some configuration. Use the approach cautiously.

Filter After Endpoint Execution

You may want to execute a filter after the endpoint executes and you have a result. Lucky for you, it’s only a matter of placing the next invocation at the beginning of your filter implementation instead of the end of the InvokeAsync method. First, let’s look at an implementation of IEndpointFilter.

public class AfterEndpointExecution : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var result = await next(context);

        if (result is MyResultWithEndpoint dp && 
            context.HttpContext.GetEndpoint() is { } e)
        {
            dp.EndpointDisplayName = e.DisplayName ?? "";
        }

        return result;
    }
}

Note the call to the next method as we retrieve the result. The result is the returned value from the endpoint. The result can be IResult or an object of your creation. Registering the filter is like all other implementations, with a call to AddEndpointFilter.

app.MapGet("/after", (string? name) => new MyResultWithEndpoint(name ?? "hi!"))
    .WithDisplayName("root endpoint (/)")
    .AddEndpointFilter<AfterEndpointExecution>();

Now you can use the resulting value to decide your ultimate response. That’s pretty awesome!

Conclusion

Endpoint Filters for ASP.NET Core Minimal APIs inch us closer to having feature parity with ASP.NET Core MVC. While, as of writing this post, ASP.NET Core MVC is still more feature-rich, Minimal APIs offer a more direct request pipeline with reduced overhead. The choice is ultimately yours, but if you’re looking to migrate from ASP.NET Core MVC, you have one more feature to help you make the leap from one to the other. I hope you enjoyed this post and better understand how to write IEndpointFilter implementations.

If you would like the code for this blog post, you can get it by checking out my GitHub repository. As always, thank you for reading and sharing my posts.