Use Endpoint Filters and Route Groups in Minimal APIs
ASP.NET Core Minimal APIs allow you to quickly create controller-less HTTP APIs with minimal dependencies. A minimal API typically has an endpoint URL and an endpoint handler. At times you want to intercept an endpoint handler so that you can add pre and post processing to the endpoint handler. That's where Endpoint Filters come into picture. When there are too many endpoints, it makes sense to group them using a common URL prefix. This is taken care by Route Groups. In this article we will examine both with a few examples.
This article assumes that you are familiar with creating Minimal APIs. If that's not the case, I suggest you read my earlier articles here and here.
Endpoint Filters allow you to wire some logic before and after an endpoint handler is executed. You can use endpoint filters for tasks such as -- logging, data validating, and enforcing an API version. Although we aren't going to discuss all these possibilities, we will quickly build a few examples so that you grasp the concept of Endpoint Filters.
Suppose, you have a simple minimal API endpoint defined in the Program.cs as shown below :
app.MapGet("/helloworld", () =>
{
app.Logger.LogInformation
("Inside helloworld endpoint handler");
return Results.Ok
("This is helloworld endpoint response");
})
As you can see, the MapGet() defines an endpoint at /helloworld and the endpoint handler logs as well as returns a string message. If you run this API, you will get this output in the browser and in the Development Server console window.
And
Now let's add three simple endpoint filters to this API.
app.MapGet("/helloworld", () =>
{
app.Logger.LogInformation
("Inside helloworld endpoint handler");
return Results.Ok
("This is helloworld endpoint response");
})
.AddEndpointFilter(async (context, next) =>
{
app.Logger.LogInformation
("Inside first filter - Forward");
var result = await next(context);
app.Logger.LogInformation
("Inside first filter - Backward");
return result;
})
.AddEndpointFilter(async (context, next) =>
{
app.Logger.LogInformation
("Inside second filter - Forward");
var result = await next(context);
app.Logger.LogInformation
("Inside second filter - Backward");
return result;
})
.AddEndpointFilter(async (context, next) =>
{
app.Logger.LogInformation
("Inside third filter - Forward");
var result = await next(context);
app.Logger.LogInformation
("Inside third filter - Backward");
return result;
});
Notice the chained calls to the AddEndpointFilter() method that takes a method containing the filter's logic or processing. The method takes two parameters -- EndpointFilterInvocationContext and EndpointFilterDelegate. The EndpointFilterInvocationContext parameters allows you to access the request and response whereas the EndpointFilterDelegate parameter is a pointer to the next filter in the chain. You need to invoke this next filter somewhere in the current filter's code.
In the above example, all the filters log a message, invoke the next filter in the chain, and again log a message. This way we will be able to see the pre and post execution of the filters.
If you run the application again, this time you will see the following log in the console window:
Observe the sequence of the log messages -- first three "forward" messages are printed starting from first to third filter. Then the message from the helloworld endpoint handler is printed. Finally, three "backward" messages are outputted from third to first filter.
In the above example only one endpoint needed the three filters. What if you need to run the filters for multiple endpoints? You can isolate the filter logic in a separate class and then use them with one or more APIs.
Let's see how that is done.
Consider the following class that represents a filer.
public class FilterOne : IEndpointFilter
{
private readonly ILogger Logger;
public FilterOne
(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.
CreateLogger<FilterOne>();
}
public async ValueTask<object>
InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation
("Inside first filter - Forward");
var result = await next(context);
Logger.LogInformation
("Inside first filter - Backward");
return result;
}
}
The FilterOne class implements IEndpointFilter interface. The InvokeAsync() method contains all the logic that you earlier write inside the AddEndpointFilter() call. The ILoggerFactory object is injected in the constructor so that we can create an ILogger for logging purposes.
On the similar lines you can create FilterTwo and FilterThree as shown below:
public class FilterTwo : IEndpointFilter
{
private readonly ILogger Logger;
public FilterTwo(ILoggerFactory loggerFactory)
{
Logger = loggerFactory
.CreateLogger<FilterTwo>();
}
public async ValueTask<object>
InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation
("Inside second filter - Forward");
var result = await next(context);
Logger.LogInformation
("Inside second filter - Backward");
return result;
}
}
public class FilterThree : IEndpointFilter
{
private readonly ILogger Logger;
public FilterThree
(ILoggerFactory loggerFactory)
{
Logger = loggerFactory
.CreateLogger<FilterThree>();
}
public async ValueTask<object>
InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation
("Inside third filter - Forward");
var result = await next(context);
Logger.LogInformation
("Inside third filter - Backward");
return result;
}
}
Since your filter logic in now isolated in classes, you need to wire them like this :
app.MapGet("/hellogalaxy", () =>
{
app.Logger.LogInformation
("Inside hellogalaxy endpoint handler");
return Results.Ok
("This is hellogalaxy endpoint response");
}).AddEndpointFilter<FilterOne>()
.AddEndpointFilter<FilterTwo>()
.AddEndpointFilter<FilterThree>();
I have changes the endpoint URL to /hellogalaxy so that our previous example remains unaffected. If you wish you can use the same endpoint to wire these filter classes.
The output will be quite similar to the previous example.
If you need to wire the filters to multiple endpoints, you will typically attach them individually with each endpoint. However, if those endpoints can be logically grouped you can organize them in a single Route Group and then attach the filters with the group.
A route group allows you to group a set of endpoints under a common route prefix. For example, you might create a route group with a route prefix of /api. You can then organize one or more endpoints under /api, say, /helloworld, /hellogalaxy, and /hellouniverse.
Take a look at the following extension method that contains three API endpoints:
public static class HelloRouteGroup
{
public static RouteGroupBuilder
MapHelloApi(this RouteGroupBuilder group,
WebApplication app)
{
group.MapGet("/helloworld", () =>
{
app.Logger.LogInformation
("Inside helloworld endpoint handler");
return Results.Ok
("This is helloworld endpoint response");
});
group.MapGet("/hellogalaxy", () =>
{
app.Logger.LogInformation
("Inside hellogalaxy endpoint handler");
return Results.Ok
("This is hellogalaxy endpoint response");
});
group.MapGet("/hellouniverse", () =>
{
app.Logger.LogInformation
("Inside hellouniverse endpoint handler");
return Results.Ok
("This is hellouniverse endpoint response");
});
return group;
}
}
The MapHelloApi() extension method extends RouteGroupBuilder in order to map three endpoints -- /helloworld, /hellogalaxy, and /hellouniverse. The app parameter is not mandatory. I have added it so that I can quickly access the Logger to log the messages.
Once MapHelloApi() is ready, you can create a route group and attach endpoint filters as shown below:
app.MapGroup("/api")
.MapHelloApi(app)
.AddEndpointFilter<FilterOne>()
.AddEndpointFilter<FilterTwo>()
.AddEndpointFilter<FilterThree>();
app.Run();
As you can see, we create /api group and then call MapHelloApi() to add three endpoints under that route group. Then we wire the three endpoint filters created earlier to the whole group using AddEndpointFilter() calls. This way the filters get attached to all the three endpoints instead of individually attaching them.
If you run the application, the output will resemble the following figure. Remember that the endpoint URLs are now /api/helloworld, /api/hellogalaxy, and /api/hellouniverse since the endpoints are grouped under /api prefix.
You can read more about Endpoint Filters and Route Groups in the official documentation here and here.
That's it for now! Keep coding!!