ASP.NET Core Web APIs · Minimal APIs Done Right
Endpoint Filters and Middleware
Add cross-cutting behaviour with middleware for every request and endpoint filters for specific endpoints, in the right order.
What you'll build
A middleware that adds a request ID header and measures timing for every request, and an endpoint filter on one route group that rejects requests without an API key, with the order in which they run made visible.
The idea
- Middleware (
app.Use(...)) runs for every request in pipeline order; callnext(context)to continue. - Endpoint filters (
AddEndpointFilter) run only for the endpoints they're attached to, after routing has selected the endpoint, with access to bound arguments. - Order matters: exception handling first, then HTTPS redirection, routing, authentication and authorization, then endpoints.
The code
var log = new List<string>();
var app = WebApplication.Create();
app.Use(async (context, next) =>
{
var id = context.Request.Headers["X-Request-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("N")[..8];
context.Response.Headers["X-Request-Id"] = id;
log.Add($"{id} middleware: before");
await next(context);
log.Add($"{id} middleware: after, status {context.Response.StatusCode}");
});
app.MapGet("/health", () => "ok");
var admin = app.MapGroup("/admin").AddEndpointFilter(async (ctx, next) =>
{
var id = ctx.HttpContext.Response.Headers["X-Request-Id"];
if (ctx.HttpContext.Request.Headers["X-Api-Key"] != "secret-key")
{
log.Add($"{id} filter: rejected");
return Results.Unauthorized();
}
log.Add($"{id} filter: allowed");
return await next(ctx);
});
admin.MapGet("/stats", () => new { orders = 42 });
app.Urls.Add("http://127.0.0.1:0");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
foreach (var (path, id, key) in new[] { ("/health", "r1", ""), ("/admin/stats", "r2", ""), ("/admin/stats", "r3", "secret-key") })
{
var request = new HttpRequestMessage(HttpMethod.Get, path);
request.Headers.Add("X-Request-Id", id);
if (key != "") request.Headers.Add("X-Api-Key", key);
await http.SendAsync(request);
}
await app.StopAsync();
Console.WriteLine(string.Join("\n", log));
Output
r1 middleware: before r1 middleware: after, status 200 r2 middleware: before r2 filter: rejected r2 middleware: after, status 401 r3 middleware: before r3 filter: allowed r3 middleware: after, status 200
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Middleware registered after
MapGetcalls in a way that never runs for those endpoints, or authentication after authorization. - Implementing authorization as custom middleware instead of the built-in authorization policies.
- Heavy work (database calls) in middleware that runs for health checks and static files too.
Key points
- Middleware: every request, in pipeline order.
- Endpoint filters: selected endpoints, with access to arguments.
- Keep the standard order: exceptions, HTTPS, routing, auth, endpoints.
Further reading: ASP.NET Core middleware · Filters in minimal API apps
Practice questions
When should you use an endpoint filter instead of middleware?
Answer
When the behaviour applies to specific endpoints or groups and may need the bound arguments, rather than to every request.
Why does middleware order matter?
Answer
Each middleware wraps the next; for example exception handling must come first to catch errors from later middleware, and authentication must run before authorization.
What happens if middleware doesn't call next?
Answer
The pipeline short-circuits: later middleware and the endpoint don't run, and the middleware's response is returned.
Open in the app to mark lessons complete, take notes and track your progress.