Claude
Skills
Sign in
Back

middleware-patterns

Included with Lifetime
$97 forever

Custom middleware patterns for ASP.NET Core applications. Covers request/response pipeline, middleware ordering, conditional middleware, IMiddleware factory pattern, IExceptionHandler (.NET 8+), and reusable middleware components. Use when creating custom middleware in ASP.NET Core applications, understanding middleware pipeline ordering, or implementing cross-cutting concerns like logging, authentication, and caching.

General

What this skill does


# ASP.NET Core Middleware Patterns

## Rationale

Middleware is the backbone of ASP.NET Core request processing. Properly designed middleware enables cross-cutting concerns like logging, authentication, and caching. Understanding the pipeline order and middleware patterns is critical for building robust applications.

---

## Pipeline Ordering

Middleware executes in the order it is registered. The order is critical -- placing middleware in the wrong position causes subtle bugs.

### Recommended Order

```csharp
var app = builder.Build();

// 1. Exception handling (outermost -- catches everything below)
app.UseExceptionHandler("/error");

// 2. HSTS (before any response is sent)
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

// 3. HTTPS redirection
app.UseHttpsRedirection();

// 4. Static files (short-circuits for static content before routing)
app.UseStaticFiles();

// 5. Routing (matches endpoints but does not execute them yet)
app.UseRouting();

// 6. CORS (must be after routing, before auth)
app.UseCors();

// 7. Authentication (identifies the user)
app.UseAuthentication();

// 8. Authorization (checks permissions against the matched endpoint)
app.UseAuthorization();

// 9. Custom middleware (runs after auth, before endpoint execution)
app.UseRequestLogging();

// 10. Endpoint execution (terminal -- executes the matched endpoint)
app.MapControllers();
app.MapRazorPages();
```

### Why Order Matters

| Mistake | Consequence |
|---------|-------------|
| `UseAuthorization()` before `UseRouting()` | Authorization has no endpoint metadata -- all requests pass |
| `UseCors()` after `UseAuthorization()` | Preflight requests fail because they lack auth tokens |
| `UseExceptionHandler()` after custom middleware | Exceptions in custom middleware are unhandled |
| `UseStaticFiles()` after `UseAuthorization()` | Static files require authentication unnecessarily |

---

## Pattern 1: Convention-Based Middleware

Convention-based middleware uses a constructor with `RequestDelegate` and an `InvokeAsync` method.

```csharp
public sealed class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            _logger.LogInformation(
                "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
                context.Request.Method,
                context.Request.Path,
                stopwatch.ElapsedMilliseconds,
                context.Response.StatusCode);
        }
    }
}

public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
        => app.UseMiddleware<RequestTimingMiddleware>();
}

// Usage in Program.cs
app.UseRequestTiming();
```

---

## Pattern 2: Factory-Based (IMiddleware)

For middleware that requires scoped services, implement `IMiddleware`. This uses DI to create middleware instances per-request:

```csharp
public sealed class TenantMiddleware : IMiddleware
{
    private readonly TenantDbContext _db;

    public TenantMiddleware(TenantDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();

        if (tenantId is not null)
        {
            var tenant = await _db.Tenants.FindAsync(tenantId);
            context.Items["Tenant"] = tenant;
        }

        await next(context);
    }
}

// IMiddleware requires explicit DI registration
builder.Services.AddScoped<TenantMiddleware>();
app.UseMiddleware<TenantMiddleware>();
```

### Convention-Based vs IMiddleware

| Aspect | Convention-based | `IMiddleware` |
|--------|-----------------|---------------|
| Lifetime | Singleton (created once) | Per-request (from DI) |
| Scoped services | Via `InvokeAsync` parameters only | Via constructor injection |
| Registration | `UseMiddleware<T>()` only | Requires `services.Add*<T>()` + `UseMiddleware<T>()` |
| Performance | Slightly faster | Resolved from DI each request |

---

## Pattern 3: Inline Middleware

For simple, one-off logic:

### app.Use -- Pass-Through

```csharp
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Request-Id"] = context.TraceIdentifier;
    await next(context);
});
```

### app.Run -- Terminal

```csharp
app.Run(async context =>
{
    await context.Response.WriteAsync("Fallback response");
});
```

### app.Map -- Branch by Path

```csharp
app.Map("/api/diagnostics", diagnosticApp =>
{
    diagnosticApp.Run(async context =>
    {
        var data = new
        {
            MachineName = Environment.MachineName,
            Timestamp = DateTimeOffset.UtcNow
        };
        await context.Response.WriteAsJsonAsync(data);
    });
});
```

---

## Pattern 4: Short-Circuit Logic

Middleware can short-circuit the pipeline by not calling `next()`.

### Request Validation

```csharp
public sealed class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _expectedKey;

    public ApiKeyMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _expectedKey = config["ApiKey"]
            ?? throw new InvalidOperationException("ApiKey configuration is required");
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue("X-Api-Key", out var providedKey)
            || !string.Equals(providedKey, _expectedKey, StringComparison.Ordinal))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsJsonAsync(new
            {
                Error = "Invalid or missing API key"
            });
            return; // Short-circuit
        }

        await _next(context);
    }
}
```

### Feature Flag Gate

```csharp
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/beta"),
    betaApp =>
    {
        betaApp.Use(async (context, next) =>
        {
            var featureManager = context.RequestServices
                .GetRequiredService<IFeatureManager>();

            if (!await featureManager.IsEnabledAsync("BetaFeatures"))
            {
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                return;
            }

            await next(context);
        });
    });
```

---

## Pattern 5: Request and Response Manipulation

### Reading the Request Body

```csharp
public sealed 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)
    {
        context.Request.EnableBuffering();

        if (context.Request.ContentLength > 0 && context.Request.ContentLength < 64_000)
        {
            context.Request.Body.Position = 0;
            using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
            var body = await reader.ReadToEndAsync();
            _logger.LogDebug("Request body for {Path}: {Body}", context.Request.Path, body);
            context.Request.Body.Position = 0;
        }

        await _next(context);
    }
}
```

### Modifying the Response

```csharp
public async Task InvokeAsync(HttpContext context)
{
    var originalBodyStream = context.Response.Body;

    using var re
Files: 1
Size: 18.5 KB
Complexity: 25/100
Category: General

Related in General