Between the moment Kestrel has read a request and the call of your handler, quite a lot happens: an error should be caught, an http
address redirected to https, a file from wwwroot served without any endpoint knowing about it. Each of these tasks is handled by a
piece of code of its own, a middleware, and together they form the pipeline. In this lesson, you see how the pipeline works, why
its order matters and how you write your own middleware.
The Kladde’s own middleware writes a line to the log for every request. For that, you get to know logging with ILogger, and above
all what never belongs in a log. A key ending up in the log is one of the most common mistakes in web applications, and it happens
without any bad intent at all: you will see it in exercise 2 in a small app that does exactly that.
The pipeline
A middleware is a function that gets the HttpContext of a request and can call the next middleware, or not. In k2-practice/2-3,
create the file pipeline.cs. The output with Console.WriteLine is only for watching; how to log properly comes further below.
// pipeline.cs: three middleware and an endpoint
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("1 before");
await next(context);
Console.WriteLine("1 after");
});
app.Use(async (context, next) =>
{
Console.WriteLine("2 before");
await next(context);
Console.WriteLine($"2 after, status {context.Response.StatusCode}");
});
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/internal"))
{
Console.WriteLine("3 answers itself");
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
await next(context);
});
app.MapGet("/", () =>
{
Console.WriteLine("Endpoint");
return "Hello from the pipeline";
});
app.MapGet("/internal/numbers", () => "Nobody reaches this line.");
app.Run();
Start the app and send two requests:
curl -i http://localhost:5000/
curl -i http://localhost:5000/internal/numbers
The first answers with 200 and “Hello from the pipeline”, the second with 403 Forbidden and no content. In the app’s terminal, it says:
1 before
2 before
Endpoint
2 after, status 200
1 after
1 before
2 before
3 answers itself
2 after, status 403
1 after
That shows you the three rules of the pipeline:
app.Use(…)attaches a middleware, in the order of the lines. The first one attached gets the request first.await next(context)calls the rest of the chain, so all later middleware and, at the end, the endpoint. What comes before runs on the way in, what comes after runs on the way back, and in reverse order. Afternext, the middleware knows the status code.- Whoever doesn’t call
nextends the chain. Middleware 3 answers for/internalitself; the endpoint never runs, but the outer middleware still see the response on the way back.
A picture helps: the pipeline is like a set of onion layers. The request goes from the outside to the inside through every layer to the endpoint, and the response goes back out from the inside to the outside. What lies outside sees everything; what lies inside sees only what the outer ones let through.
WebApplication inserts a few layers itself, without you writing them. In the Development environment, the developer exception
page comes right on the outside, which shows an error with a stack trace. Right after it comes routing: it looks for the matching
endpoint at the very start, which is why your middleware knows through context.GetEndpoint() which endpoint is about to run. The
endpoint runs on the very inside. If there is none, the end of the pipeline answers with 404.