Middleware

Every request to an ASP.NET Core app passes through a pipeline of middleware — small pieces of code that run in sequence, each able to inspect or modify the request, short-circuit it entirely, or pass it along to the next piece.

Writing inline middleware

app.Use registers a middleware delegate directly in Program.cs:

C# Program.cs
app.Use(async (context, next) =>
{
    Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");
    await next();
    Console.WriteLine($"Response: {context.Response.StatusCode}");
});

app.MapGet("/", () => "Hello!");

app.Run();
Terminal, after GET /
Request: GET /
Response: 200

await next() is what passes control to the next middleware in line (and eventually to the route handler) — everything written before await next() runs on the way in, and everything after it runs on the way back out, once a response exists.

Order matters

Middleware runs in exactly the order it's registered. A handful of built-in middleware components have to go in a specific relative order to work correctly:

C# Program.cs
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

UseAuthentication has to run before UseAuthorization — authorization needs to know who the request claims to be (which authentication establishes) before it can decide whether that identity is allowed to do something. Registering them in the reverse order compiles fine and fails silently at runtime: every request gets treated as unauthenticated by the authorization check.

Short-circuiting the pipeline

Middleware can also stop the pipeline entirely by never calling next():

C# Program.cs
app.Use(async (context, next) =>
{
    if (!context.Request.Headers.ContainsKey("X-Api-Key"))
    {
        context.Response.StatusCode = 401;
        await context.Response.WriteAsync("Missing API key");
        return; // next() is never called
    }
    await next();
});
Terminal
$ curl -i http://localhost:5225/
HTTP/1.1 401 Unauthorized
Missing API key

Because next() is never awaited, every later middleware — routing, controllers, all of it — is skipped entirely for a request missing that header.

Note: real projects almost always reach for pre-built middleware (like the built-in authentication/authorization/CORS middleware, or a package) rather than hand-writing checks like the example above — inline middleware is mainly useful for logging, custom headers, and understanding what's actually happening in the pipeline before you rely on someone else's package to do it.