Skip to main content
ANVISoftware Solutions
Lesson 4 of 23Intermediate16 min

Middleware

By the end of this lesson

Write middleware and explain how a request passes through the pipeline.

Middleware is a component that sits in the path of every request. It receives the request, may do something with it, passes it along, and gets a second turn when the response comes back.

The pipeline is not a list being iterated. Each component holds a reference to the next one and calls it, so the chain is a set of nested calls. That is why one component can act on the way in and again on the way out: the code after the call to next runs once everything deeper has finished.

This is the mechanism behind most of what the framework does for you. Exception handling, HTTPS redirection, static files, authentication, authorization and routing are all middleware, registered the same way as anything you write.

Timing and logging middleware, acting on both sides of next
C#
using System.Diagnostics;

var app = builder.Build();
var logger = app.Services.GetRequiredService<ILogger<Program>>();

app.Use(async (context, next) =>
{
    // On the way in.
    var started = Stopwatch.GetTimestamp();
    context.Response.Headers["X-Correlation-Id"] = Guid.NewGuid().ToString();

    await next(context);

    // On the way out.
    var elapsed = Stopwatch.GetElapsedTime(started);
    logger.LogInformation(
        "{Method} {Path} responded {StatusCode} in {ElapsedMs} ms",
        context.Request.Method,
        context.Request.Path,
        context.Response.StatusCode,
        elapsed.TotalMilliseconds);
});
  • app.Use takes a function with two arguments: the context for this request, and a delegate that represents the rest of the pipeline.
  • Everything above the call to next runs before any later middleware and before your endpoint. The stopwatch starts here because this is the earliest point this component sees the request.
  • Setting a response header has to happen here too, before the response starts. Once the first byte has been sent, the headers are fixed.
  • await next(context) hands control to the next component. This method is paused at that line, with its local variables intact, until everything downstream has finished.
  • Everything below next runs on the way out. The status code is now known, because the endpoint has already set it, so this is the only place this log line could read it.
  • Resolving the logger from app.Services is acceptable for a lambda registered at startup, because this one instance is shared by every request. A class-based middleware would take ILogger in its constructor instead.
Three components and an endpoint, in execution order
Text
A: on the way in
  B: on the way in
    C: on the way in
      endpoint produces the response
    C: on the way out
  B: on the way out
A: on the way out
  • The indentation is the important part: each component wraps the ones registered after it.
  • A component registered first sees the request earliest and the response latest. That is why exception handling belongs at the top — it is the outermost wrapper.
Short-circuiting: the component that does not call next
C#
app.Use(async (context, next) =>
{
    if (!context.Request.Headers.ContainsKey("X-Api-Key"))
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        await context.Response.WriteAsJsonAsync(new { error = "An API key is required." });
        return; // next is never called
    }

    await next(context);
});
  • This is a teaching example of the mechanism, not a recommendation for real security. Proper authentication is a later module.
  • The check happens before routing, so the request is refused without the framework working out which endpoint it was for.
  • Setting the status code and writing a body is the entire response. Nothing registered deeper in the pipeline runs at all.
  • The return statement is the short circuit. Leaving out the call to next is how you stop a request, and it is exactly how caching, redirection and rate limiting middleware work.
  • Components registered before this one still get their turn on the way out. Short-circuiting stops the request going deeper; it does not undo what already ran.

The registration methods, and what each one is for:

app.Use
Adds a component that may pass the request on. The normal choice for anything you write.
app.Run
Adds a component that never passes the request on. It ends the pipeline, so anything registered after it is unreachable.
app.UseWhen
Runs extra middleware only when a condition holds, then rejoins the main pipeline. Useful for behaviour that applies to one path prefix.
app.Map and app.MapWhen
Branches the pipeline. The branch is a separate chain and does not rejoin the main one, so everything the branch needs has to be registered inside it.
app.UseMiddleware of a class
Registers a class rather than a lambda. The class takes a RequestDelegate in its constructor and exposes an InvokeAsync method taking HttpContext.

Summary

  • Middleware components are nested calls rather than a list: each one calls the next
  • Code before next runs on the way in; code after it runs on the way out, when the status code is known
  • Not calling next short-circuits the request, which is how rejection, caching and redirection work
  • Response headers and the status code can only be changed before the response starts
  • Middleware fits concerns that apply to every request; endpoint-specific work belongs in a filter or the endpoint

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Write it as a class

Rewrite the correlation id middleware as a class with a constructor and an InvokeAsync method, and register it with UseMiddleware.

Have it read the incoming X-Correlation-Id header if the caller sent one, and generate a value only when they did not. Echo the value on the response.

Show solution

The class form matters for three reasons: it keeps Program.cs readable, it can take its dependencies through the constructor, and it can be tested by calling InvokeAsync with a DefaultHttpContext and a stub for next.

Note where the dependency goes. The constructor is called once, so anything injected there must be safe to share for the life of the process. A scoped service belongs on the InvokeAsync signature, where the framework resolves it from the current request's scope.

Honouring an incoming correlation id rather than always generating one is what makes this useful across services. When three services log the same value, one search finds the whole journey of a request.

C#
public sealed class CorrelationIdMiddleware
{
    private const string HeaderName = "X-Correlation-Id";

    private readonly RequestDelegate _next;
    private readonly ILogger<CorrelationIdMiddleware> _logger;

    public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var incoming = context.Request.Headers[HeaderName].FirstOrDefault();
        var correlationId = string.IsNullOrWhiteSpace(incoming)
            ? Guid.NewGuid().ToString()
            : incoming;

        context.Response.Headers[HeaderName] = correlationId;

        using (_logger.BeginScope("CorrelationId {CorrelationId}", correlationId))
        {
            await _next(context);
        }
    }
}

// Program.cs, in the app phase
app.UseMiddleware<CorrelationIdMiddleware>();

Think about it

Who still runs after a short circuit?

Middleware A logs a line, calls next, then logs a second line. Middleware B is registered after A and short-circuits with a 403 without calling next.

Which of A's log lines appear, and what does the second one report as the status code?

Show solution

Both of A's lines appear. A's call to next returned as soon as B decided not to continue, so A's second half runs as normal.

The status code A reports is 403, because B set it before returning. This is what makes the nested model useful: a logging component records every outcome, including ones produced by components it wraps, without knowing anything about them.

The components that do not run are the ones registered after B. A short circuit stops the request going deeper; it has no effect on what has already run.

Saved in this browser only.