Core API Middleware

Every HTTP request that enters the BookStore API passes through a series of components before it reaches a controller. Every response passes through the same components in reverse order. These components are called middleware. Middleware gives the ability to inspect, modify, short-circuit, or log requests and responses at any point in the pipeline.

What Is Middleware?

Middleware is software assembled into the application pipeline to handle requests and responses. Each middleware component can:

  • Perform work before the next component runs
  • Pass the request to the next middleware in the pipeline
  • Perform work after the next component has completed
  • Optionally short-circuit the pipeline and return a response immediately

The Middleware Pipeline

HTTP Request
     |
     v
┌─────────────────────────────────────────┐
│  Middleware 1: HTTPS Redirection        │
│     ↓ (passes request forward)          │
│  Middleware 2: Authentication           │
│     ↓                                   │
│  Middleware 3: Authorization            │
│     ↓                                   │
│  Middleware 4: Routing                  │
│     ↓                                   │
│  Controller Action (endpoint)           │
│     ↑ (response flows back up)          │
│  Middleware 4: Routing                  │
│     ↑                                   │
│  Middleware 3: Authorization            │
│     ↑                                   │
│  Middleware 2: Authentication           │
│     ↑                                   │
│  Middleware 1: HTTPS Redirection        │
└─────────────────────────────────────────┘
     |
     v
HTTP Response → Client

This is called the request-response pipeline. Each middleware decides whether to call the next one. If it does not, the pipeline is short-circuited and the response goes back to the client immediately.

Built-in Middleware in the BookStore API

// Program.cs – middleware registered in order
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();         // Serves the swagger.json file
    app.UseSwaggerUI();       // Serves the Swagger browser UI
}

app.UseHttpsRedirection();    // Redirects HTTP → HTTPS
app.UseAuthentication();      // Validates JWT tokens (added later)
app.UseAuthorization();       // Checks user permissions
app.MapControllers();         // Routes requests to controllers
MiddlewareWhat It Does
UseSwagger()Generates the OpenAPI specification JSON
UseSwaggerUI()Serves the interactive Swagger browser
UseHttpsRedirection()Forces all requests to use HTTPS
UseAuthentication()Reads and validates the JWT token from headers
UseAuthorization()Checks if the authenticated user has permission
MapControllers()Routes the request to the matching controller action

Middleware Order Matters

The order middleware is registered in Program.cs is the order it runs. This matters. Authentication must run before Authorization. Routing must run before controllers are invoked.

// WRONG ORDER (will cause bugs):
app.UseAuthorization();     // tries to check permissions...
app.UseAuthentication();    // ...before user is authenticated!

// CORRECT ORDER:
app.UseAuthentication();    // 1. Identify who the user is
app.UseAuthorization();     // 2. Check what they can do

Writing Custom Middleware

Custom middleware is useful for cross-cutting concerns — tasks that apply to every request. A common use case is request logging: recording what endpoint was called and how long it took.

Option 1: Inline Middleware with app.Use()

// Program.cs
app.Use(async (context, next) =>
{
    var startTime = DateTime.UtcNow;
    var path = context.Request.Path;
    var method = context.Request.Method;

    await next();   // Call the next middleware in the pipeline

    var duration = (DateTime.UtcNow - startTime).TotalMilliseconds;
    Console.WriteLine($"{method} {path} completed in {duration}ms | Status: {context.Response.StatusCode}");
});

Sample output in the console:

GET /api/books completed in 12ms | Status: 200
POST /api/books completed in 45ms | Status: 201
GET /api/books/999 completed in 8ms | Status: 404

Option 2: Class-Based Middleware (Recommended)

For more complex logic, middleware is better written as a separate class:

// Middleware/RequestLoggingMiddleware.cs
namespace BookStoreAPI.Middleware
{
    public class RequestLoggingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<RequestLoggingMiddleware> _logger;

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

        public async Task InvokeAsync(HttpContext context)
        {
            var startTime = DateTime.UtcNow;
            var method = context.Request.Method;
            var path = context.Request.Path;

            _logger.LogInformation("→ Incoming: {Method} {Path}", method, path);

            await _next(context);   // Call next middleware

            var duration = (DateTime.UtcNow - startTime).TotalMilliseconds;
            _logger.LogInformation("← Completed: {Method} {Path} | {StatusCode} | {Duration}ms",
                method, path, context.Response.StatusCode, duration);
        }
    }
}

Register the custom middleware in Program.cs:

// Program.cs
app.UseMiddleware<RequestLoggingMiddleware>();   // ← add before routing
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

Short-Circuiting the Pipeline

Middleware can stop the pipeline and return a response immediately without calling the next middleware. This is useful for blocking unauthorized or invalid requests early.

app.Use(async (context, next) =>
{
    // Block requests without an API key header
    if (!context.Request.Headers.ContainsKey("X-Api-Key"))
    {
        context.Response.StatusCode = 401;
        await context.Response.WriteAsync("API key is missing.");
        return;   // ← pipeline stops here, next() is NOT called
    }

    await next();   // ← pipeline continues
});

Terminal Middleware with app.Run()

app.Run() always terminates the pipeline. It never calls the next middleware. This is used at the very end of the pipeline.

app.Run(async (context) =>
{
    // This runs last — no next() available
    await context.Response.WriteAsync("Request handled.");
});

Middleware Lifecycle Summary

app.Use()     → Can call next() OR short-circuit
app.Run()     → Always terminates (no next())
app.Map()     → Branches the pipeline based on URL path
app.UseMiddleware<T>() → Registers class-based middleware

BookStore API Middleware Order

var app = builder.Build();

// Development only
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Custom middleware (runs first for every request)
app.UseMiddleware<RequestLoggingMiddleware>();

// Built-in middleware
app.UseHttpsRedirection();
app.UseAuthentication();       // Added in JWT topic
app.UseAuthorization();
app.MapControllers();

app.Run();

Key Points

  • Middleware is software that processes every HTTP request before it reaches the controller and every response before it leaves the server.
  • The pipeline is bidirectional — middleware runs on the way in (request) and on the way back out (response).
  • Middleware order matters: Authentication must come before Authorization.
  • Custom middleware is written using app.Use() for simple cases or a dedicated class with InvokeAsync for complex cases.
  • Calling return without calling next() short-circuits the pipeline and stops further processing.

Leave a Comment