Minimal API Route Handler Filters
source link: https://khalidabuhakmeh.com/minimal-api-route-handler-filters
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Minimal API Route Handler Filters
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 functionality. 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 IRouteHandlerFilter
.
In this post, we’ll see how to write several kinds of IRouteHandlerFilter
implementations and how they can help reduce the repetition in your Minimal API apps.
What’s An IRouteHandlerFilter?
An IRouteHandlerFilter
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 IRouteHandlerFilter
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 IRouteHandlerFilter
has one method to implement. Still, you have access to the HttpContext
through the RouteHandlerInvocationContext,
and the subsequent RouteHandlerFilterDelegate
in the request pipeline provides you with all the ingredients you need to do what you wish.
namespace Microsoft.AspNetCore.Http;
public interface IRouteHandlerFilter
{
ValueTask<object?> InvokeAsync(
RouteHandlerInvocationContext context,
RouteHandlerFilterDelegate next);
}
You need to invoke the AddFilter
method on your endpoint definition to register a route filter.
app.MapGet("/before/{name?}", (string? name) =>
new MyResult(name ?? "Hi!"))
.AddFilter<BeforeEndpointExecution>();
Let’s get into some examples of IRouteHandlerFilter
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 IRouteHandlerFilter
implementation that runs before an endpoint executes.
public class BeforeEndpointExecution : IRouteHandlerFilter
{
public async ValueTask<object?> InvokeAsync(
RouteHandlerInvocationContext context,
RouteHandlerFilterDelegate 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!"))
.AddFilter<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: IRouteHandlerFilter
{
public ValueTask<object?> InvokeAsync(
RouteHandlerInvocationContext context,
RouteHandlerFilterDelegate next)
{
// because YOLO!
return new ValueTask<object?>(Results.Json(new { Fizz = "Buzz" }));
}
}
Note that we’re missing the call to the next RouteHandlerFilterDelegate
. 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")
.AddFilter<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 IRouteHandlerFilter
.
public class AfterEndpointExecution : IRouteHandlerFilter
{
public async ValueTask<object?> InvokeAsync(
RouteHandlerInvocationContext context,
RouteHandlerFilterDelegate 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 AddFilter
.
app.MapGet("/after", (string? name) => new MyResultWithEndpoint(name ?? "hi!"))
.WithDisplayName("root endpoint (/)")
.AddFilter<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 IRouteHandlerFilter
implementations.
If you’d 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.
About Khalid Abuhakmeh
Khalid is a developer advocate at JetBrains focusing on .NET technologies and tooling.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK