Route groups and endpoint filters

ASP.NET Core · Advanced · 12 min

Groups and filters keep larger minimal API apps structured without repeating auth, prefixes and checks.

Route groups share a prefix and conventions among endpoints:

var todos = app.MapGroup("/api/todos").WithTags("Todos").RequireAuthorization();
todos.MapGet("/", GetAll);
todos.MapGet("/{id:int}", GetById);

Anything you can apply to one endpoint (authorization, rate limiting, caching, OpenAPI metadata, filters) can be applied to a group, and groups can be nested. A common structure is one static class per feature with a MapTodoEndpoints(this IEndpointRouteBuilder app) extension method that creates the group, keeping Program.cs tiny.

Endpoint filters run around a minimal API handler, after binding, much like MVC action filters:

.AddEndpointFilter(async (ctx, next) => { /* before */ var result = await next(ctx); /* after */ return result; })

ctx.Arguments / ctx.GetArgument<T>(index) give access to the bound parameters, and ctx.HttpContext to everything else. Return a result early to short-circuit (for example a validation problem), or call next and transform its result.

Filters vs middleware: middleware sees every request but knows nothing about the endpoint's parameters; filters run only for the endpoints they're attached to and see typed arguments. Use filters for argument validation, auditing specific operations or wrapping responses; use middleware for global concerns.

Filters added to a group run for all its endpoints, in the order they're added (outer first). Reusable filters implement IEndpointFilter.

Example

var app = WebApplication.Create(args);
app.MapTodoEndpoints(); app.Run();
// GET /api/todos/0 -> 400 {"title":"Bad Request","status":400,"detail":"id must be positive"}
// GET /api/todos/5 -> 200 "todo 5", header X-Audit: GET /api/todos/5

static class TodoEndpoints
{
    public static void MapTodoEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/todos")
            .WithTags("Todos")
            .AddEndpointFilter(async (ctx, next) =>           // runs for every endpoint
            {
                var req = ctx.HttpContext.Request;
                ctx.HttpContext.Response.Headers["X-Audit"] = $"{req.Method} {req.Path}";
                return await next(ctx);
            });

        group.MapGet("/{id:int}", (int id) => $"todo {id}")
             .AddEndpointFilter(async (ctx, next) =>          // validates one argument
                 ctx.GetArgument<int>(0) <= 0
                     ? TypedResults.Problem("id must be positive", statusCode: 400)
                     : await next(ctx));
    }
}

Remember

  • MapGroup shares prefixes and conventions (auth, tags, filters)
  • Organize features as extension methods that map their own group
  • Endpoint filters see bound arguments and can short-circuit
  • Use middleware for global concerns, filters for endpoint-specific ones

Check yourself

  1. What can an endpoint filter do that middleware cannot easily do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters