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 InvokeAsync is 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 RouteGroupBuilder but 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

  • AddEndpointFilter called on wrong endpoint or route group
  • Filter registered before routes are mapped (registration order matters)
  • IEndpointFilter implementation does not call next(context)
  • Route group filter not inherited by nested groups
  • Filter attached to MapMethods variant that does not match request

Step-by-Step Fix

  1. 1.Attach filter directly to the endpoint:
  2. 2.```csharp
  3. 3.// CORRECT - filter attached to the specific endpoint
  4. 4.app.MapGet("/api/users/{id}", async (int id, IUserService service) =>
  5. 5.{
  6. 6.var user = await service.GetUserAsync(id);
  7. 7.return Results.Ok(user);
  8. 8.})
  9. 9..AddEndpointFilter(async (context, next) =>
  10. 10.{
  11. 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. 1.Create a reusable filter class implementing IEndpointFilter:
  2. 2.```csharp
  3. 3.public class ValidationFilter<TRequest> : IEndpointFilter where TRequest : class
  4. 4.{
  5. 5.public async ValueTask<object?> InvokeAsync(
  6. 6.EndpointFilterInvocationContext context,
  7. 7.EndpointFilterDelegate next)
  8. 8.{
  9. 9.// Find the argument that matches TRequest
  10. 10.var argument = context.Arguments
  11. 11..OfType<TRequest>()
  12. 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. 1.Apply filter to a route group for multiple endpoints:
  2. 2.```csharp
  3. 3.var api = app.MapGroup("/api")
  4. 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. 1.Debug filter registration and execution:
  2. 2.```csharp
  3. 3.// Verify which endpoints have filters registered
  4. 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