Introduction
.NET 7+ introduced endpoint filters for Minimal APIs, allowing cross-cutting concerns like logging, validation, and caching to be applied per-endpoint or per-route group. Unlike middleware, which runs for all requests in the pipeline, endpoint filters execute after routing and before the endpoint handler. When an endpoint filter does not execute, it is usually because the filter was not attached to the correct endpoint, was registered on a different route group, or the filter implementation does not correctly invoke next(context).
Symptoms
- Endpoint filter registered but
InvokeAsyncis never called - Logging or validation in filter not executing for specific route
- Filter works on some endpoints but not others
- Exception thrown in filter is not caught by exception handler middleware
- Filter registered on
RouteGroupBuilderbut not applied to child endpoints
Debug filter execution:
``csharp
app.MapGet("/users", () => "Hello")
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("=== FILTER EXECUTING ==="); // Does this print?
var result = await next(context);
Console.WriteLine("=== FILTER DONE ===");
return result;
});
Common Causes
AddEndpointFiltercalled on wrong endpoint or route group- Filter registered before routes are mapped (registration order matters)
IEndpointFilterimplementation does not callnext(context)- Route group filter not inherited by nested groups
- Filter attached to
MapMethodsvariant that does not match request
Step-by-Step Fix
- 1.Attach filter directly to the endpoint:
- 2.```csharp
- 3.// CORRECT - filter attached to the specific endpoint
- 4.app.MapGet("/api/users/{id}", async (int id, IUserService service) =>
- 5.{
- 6.var user = await service.GetUserAsync(id);
- 7.return Results.Ok(user);
- 8.})
- 9..AddEndpointFilter(async (context, next) =>
- 10.{
- 11.var sw = Stopwatch.StartNew();
try { var result = await next(context); sw.Stop();
var logger = context.HttpContext.RequestServices .GetRequiredService<ILogger<Program>>();
logger.LogInformation( "Endpoint {Endpoint} executed in {ElapsedMs}ms with status {StatusCode}", context.HttpContext.GetEndpoint()?.DisplayName, sw.ElapsedMilliseconds, result.StatusCode);
return result; } catch (Exception ex) { sw.Stop(); var logger = context.HttpContext.RequestServices .GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Endpoint {Endpoint} failed after {ElapsedMs}ms", context.HttpContext.GetEndpoint()?.DisplayName, sw.ElapsedMilliseconds);
throw; } }); ```
- 1.Create a reusable filter class implementing IEndpointFilter:
- 2.```csharp
- 3.public class ValidationFilter<TRequest> : IEndpointFilter where TRequest : class
- 4.{
- 5.public async ValueTask<object?> InvokeAsync(
- 6.EndpointFilterInvocationContext context,
- 7.EndpointFilterDelegate next)
- 8.{
- 9.// Find the argument that matches TRequest
- 10.var argument = context.Arguments
- 11..OfType<TRequest>()
- 12..FirstOrDefault();
if (argument == null) { return Results.BadRequest($"Missing {typeof(TRequest).Name} parameter"); }
var validator = context.HttpContext.RequestServices .GetService<IValidator<TRequest>>();
if (validator != null) { var validationResult = await validator.ValidateAsync(argument); if (!validationResult.IsValid) { return Results.ValidationProblem( validationResult.ToDictionary()); } }
// CRITICAL: Must call next to continue the pipeline return await next(context); } }
// Usage app.MapPost("/api/users", async (UserCreateDto dto, IUserService service) => { var user = await service.CreateUserAsync(dto); return Results.Created($"/api/users/{user.Id}", user); }) .AddEndpointFilter<ValidationFilter<UserCreateDto>>(); ```
- 1.Apply filter to a route group for multiple endpoints:
- 2.```csharp
- 3.var api = app.MapGroup("/api")
- 4..AddEndpointFilter<AuthenticationFilter>();
// Filter applies to all endpoints in this group api.MapGet("/users", GetUsers); api.MapPost("/users", CreateUser); api.MapGet("/users/{id}", GetUserById);
// Nested groups inherit parent filters var admin = api.MapGroup("/admin") .AddEndpointFilter<AuthorizationFilter>();
// admin endpoints get BOTH AuthenticationFilter and AuthorizationFilter admin.MapGet("/users", GetAllUsers); admin.MapDelete("/users/{id}", DeleteUser);
// Filter class public class AuthenticationFilter : IEndpointFilter { public async ValueTask<object?> InvokeAsync( EndpointFilterInvocationContext context, EndpointFilterDelegate next) { var authHeader = context.HttpContext.Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) { return Results.Unauthorized(); }
// Validate token var tokenHandler = context.HttpContext.RequestServices .GetRequiredService<ITokenValidator>();
var isValid = await tokenHandler.ValidateTokenAsync(authHeader["Bearer ".Length..]); if (!isValid) { return Results.Unauthorized(); }
return await next(context); } } ```
- 1.Debug filter registration and execution:
- 2.```csharp
- 3.// Verify which endpoints have filters registered
- 4.var endpointDataSource = app.Services.GetRequiredService<EndpointDataSource>();
foreach (var endpoint in endpointDataSource.Endpoints) { if (endpoint is RouteEndpoint routeEndpoint) { var filters = endpoint.Metadata .GetMetadata<IEndpointFilterMetadata>()?.Filters;
Console.WriteLine($"Route: {routeEndpoint.RoutePattern.RawText}"); Console.WriteLine($"Filters: {filters?.Count ?? 0}");
if (filters != null) { foreach (var filter in filters) { Console.WriteLine($" - {filter.GetType().Name}"); } } } }
// Check if filter is in DI container var filterTypes = app.Services.GetServices<IEndpointFilter>(); Console.WriteLine($"Registered filters: {filterTypes.Count()}"); ```
Prevention
- Always call
next(context)in filter implementations, otherwise the endpoint handler never runs - Use filter classes instead of inline lambdas for complex logic and testability
- Apply filters at the route group level when multiple endpoints need the same behavior
- Add logging at the entry and exit of each filter for troubleshooting
- Test filters with unit tests using
DefaultEndpointFilterInvocationContext - Remember that endpoint filters run AFTER middleware — do not duplicate middleware logic
- Document which filters apply to which route groups for team clarity