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.

Create a free account to save this page, take notes and track your progress. Sign in

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; call next(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

Middleware for all requests, a filter for one group
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 MapGet calls 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.

Was this page helpful?