Skip to content
cccskills
FreePublished on GitHub

dotnet-middleware-patterns

Builds ASP.NET Core middleware. Pipeline ordering, short-circuit, exception handling.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md20.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

dotnet-middleware-patterns

ASP.NET Core middleware patterns for the HTTP request pipeline. Covers correct ordering, writing custom middleware as classes or inline delegates, short-circuit logic, request/response manipulation, exception handling middleware, and conditional middleware registration.

Scope

  • Correct middleware pipeline ordering and common ordering mistakes
  • Custom middleware classes (convention-based and IMiddleware)
  • Inline middleware (Use, Run, Map)
  • Short-circuit logic for early validation and feature flags
  • Request/response body manipulation
  • Exception handling middleware (IExceptionHandler, StatusCodePages)
  • Conditional middleware (UseWhen, MapWhen)

Out of scope

  • Authentication/authorization middleware configuration -- see [skill:dotnet-api-security]
  • Observability middleware (OpenTelemetry, health checks) -- see [skill:dotnet-observability]
  • Minimal API endpoint filters -- see [skill:dotnet-minimal-apis]

Cross-references: [skill:dotnet-observability] for logging and telemetry middleware, [skill:dotnet-api-security] for auth middleware, [skill:dotnet-minimal-apis] for endpoint filters (the Minimal API equivalent of middleware).


Pipeline Ordering

Middleware executes in the order it is registered. The order is critical -- placing middleware in the wrong position causes subtle bugs (missing CORS headers, unhandled exceptions, auth bypasses).

Recommended Order


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)
// .NET 6+ calls UseRouting() implicitly if omitted; shown here for clarity
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();

```text

### 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           |

---

## Custom Middleware Classes

Convention-based middleware uses a constructor with `RequestDelegate` and an `InvokeAsync` method. This is the standard
pattern for reusable middleware.

### Basic Pattern

```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);
        }
    }
}

// Registration via extension method (conventional pattern)
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder app)
        => app.UseMiddleware<RequestTimingMiddleware>();
}

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

```csharp

### Factory-Based (IMiddleware)

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

```csharp

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

    // Scoped services can be injected directly
    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>();

// Then register in pipeline
app.UseMiddleware<TenantMiddleware>();

```text

**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 (no per-request allocation) | Resolved from DI each request (lifetime depends on registration) |

---

## Inline Middleware

For simple, one-off middleware logic, use `app.Use()`, `app.Map()`, or `app.Run()`:

### app.Use -- Pass-Through

```csharp

// Adds a header to every response, then passes to next middleware
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Request-Id"] =
        context.TraceIdentifier;

    await next(context);
});

```text

### app.Run -- Terminal

```csharp

// Terminal middleware -- does NOT call next
app.Run(async context =>
{
    await context.Response.WriteAsync("Fallback response");
});

```text

### app.Map -- Branch by Path

```csharp

// Branch the pipeline for requests matching /api/diagnostics
app.Map("/api/diagnostics", diagnosticApp =>
{
    diagnosticApp.Run(async context =>
    {
        var data = new
        {
            MachineName = Environment.MachineName,
            Timestamp = DateTimeOffset.UtcNow
        };
        await context.Response.WriteAsJsonAsync(data);
    });
});

```json

---

## Short-Circuit Logic

Middleware can short-circuit the pipeline by not calling `next()`. Use this for early validation, rate limiting, or
feature flags.

### 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 -- do NOT call _next
        }

        await _next(context);
    }
}

```text

### 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; // Short-circuit
            }

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

```text

---

## Request and Response Manipulation

### Reading the Request Body

The request body is a forward-only stream by default. Enable buffering to read it multiple times:

```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)
    {
        // Enable buffering so the body can be read multiple times
        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; // Reset for next reader
        }

        await _next(context);
    }
}

```text

### Modifying the Response

To capture or modify the response body, replace `context.Response.Body` with a `MemoryStream`:

```csharp

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

    using var responseBody = new MemoryStream();
    context.Response.Body = responseBody;

    await _next(context);

    // Read the response written by downstream middleware
    context.Response.Body.Seek(0, SeekOrigin.Begin);
    var responseText = await new StreamReader(
        context.Response.Body).ReadToEndAsync();
    context.Response.Body.Seek(0, SeekOrigin.Begin);

    // Copy back to original stream
    await responseBody.CopyToAsync(originalBodyStream);
}

```text

**Caution:** Response body replacement adds memory overhead and should only be used for diagnostics or specific
transformation requirements, not in high-throughput paths.

---

## Exception Handling Middleware

### Built-in Exception Handler

ASP.NET Core provides `UseExceptionHandler` for production-grade exception handling. This should always be the outermost
middleware:

```csharp

app.UseExceptionHandler(exceptionApp =>
{
    exceptionApp.Run(async context =>
    {
        context.Response.StatusCode =
            StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var exceptionFeature = context.Features
            .Get<IExceptionHandlerFeature>();

        var logger = context.RequestServices
            .GetRequiredService<ILogger<Program>>();
        logger.LogError(
            exceptionFeature?.Error,
            "Unhandled exception for {Path}",
            context.Request.Path);

        await context.Response.WriteAsJsonAsync(new
        {
            Error = "An internal error occurred",
            TraceId = context.TraceIdentifier
        });
    });
});

```text

### IExceptionHandler (.NET 8+)

.NET 8 introduced `IExceptionHandler` for DI-friendly, composable exception handling. Multiple handlers can be
registered and are invoked in order until one handles the exception:

```csharp

public sealed class ValidationExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken ct)
    {
        if (exception is not ValidationException validationException)
            return false; // Not handled -- pass to next handler

        context.Response.StatusCode =
            StatusCodes.Status400BadRequest;
        await context.Response.WriteAsJsonAsync(new
        {
            Error = "Validation failed",
            Details = validationException.Errors
        }, ct);

        return true; // Handled -- stop the chain
    }
}

public sealed class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken ct)
    {
        logger.LogError(exception, "Unhandled exception");

        context.Response.StatusCode =
            StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsJsonAsync(new
        {
            Error = "An internal error occurred",
            TraceId = context.TraceIdentifier
        }, ct);

        return true;
    }
}

// Register handlers in order (first match wins)
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

app.UseExceptionHandler();

```text

### StatusCodePages for Non-Exception Errors

For HTTP error status codes that are not caused by exceptions (404, 403), use `UseStatusCodePages`:

```csharp

app.UseStatusCodePagesWithReExecute("/error/{0}");

// Or inline
app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "application/json";
    await context.HttpContext.Response.WriteAsJsonAsync(new
    {
        Error = $"HTTP {context.HttpContext.Response.StatusCode}",
        TraceId = context.HttpContext.TraceIdentifier
    });
});

```text

---

## Conditional Middleware

### UseWhen -- Conditional Branch (Rejoins Pipeline)

`UseWhen` branches the pipeline based on a predicate. The branch rejoins the main pipeline after execution:

```csharp

// Only apply rate limiting headers for API routes
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    apiApp =>
    {
        // Requires builder.Services.AddRateLimiter() in service registration
        apiApp.UseRateLimiter();
    });

```text

### MapWhen -- Conditional Branch (Does Not Rejoin)

`MapWhen` creates a terminal branch that does not rejoin the main pipeline:

```csharp

// Serve a special handler for WebSocket upgrade requests
app.MapWhen(
    context => context.WebSockets.IsWebSocketRequest,
    wsApp =>
    {
        wsApp.Run(async context =>
        {
            using var ws = await context.WebSockets
                .AcceptWebSocketAsync();
            // Handle WebSocket connection
        });
    });

```text

### Environment-Specific Middleware

```csharp

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

```text

---

## Key Principles

- **Order is everything** -- middleware executes top-to-bottom for requests and bottom-to-top for responses; incorrect
  order causes auth bypasses, missing headers, and unhandled exceptions
- **Exception handler goes first** -- `UseExceptionHandler` must be the outermost middleware to catch exceptions from
  all downstream components
- **Prefer classes over inline for reusable middleware** -- convention-based middleware classes are testable,
  composable, and follow the single-responsibility principle
- **Use `IMiddleware` for scoped dependencies** -- convention-based middleware is singleton; if you need scoped services
  (DbContext, user-scoped caches), use `IMiddleware`
- **Short-circuit intentionally** -- always document why a middleware does not call `next()` and ensure it writes a
  complete response
- **Avoid response body manipulation in hot paths** -- replacing `Response.Body` with `MemoryStream` doubles memory
  usage per request

---

## Agent Gotchas

1. **Do not place `UseAuthorization()` before `UseRouting()`** -- authorization requires endpoint metadata from routing
   to evaluate policies. Without routing, all authorization checks are skipped.
2. **Do not place `UseCors()` after `UseAuthorization()`** -- CORS preflight (OPTIONS) requests do not carry auth
   tokens. If auth runs first, preflights are rejected with 401.
3. **Do not forget to call `next()` in pass-through middleware** -- forgetting `await _next(context)` silently
   short-circuits the pipeline, causing downstream middleware and endpoints to never execute.
4. **Do not read `Request.Body` without `EnableBuffering()`** -- the request body stream is forward-only by default.
   Reading it without buffering consumes it, causing model binding and subsequent reads to fail with empty data.
5. **Do not register `IMiddleware` implementations without DI registration** -- unlike convention-based middleware,
   `IMiddleware` requires explicit `services.AddScoped<T>()` or `services.AddTransient<T>()`. Without it,
   `UseMiddleware<T>()` throws at startup.
6. **Do not write to `Response.Body` after calling `next()` if downstream middleware has already started the response**
   -- once headers are sent (response has started), modifications throw `InvalidOperationException`. Check
   `context.Response.HasStarted` before writing.

---

## Knowledge Sources

Middleware patterns in this skill are grounded in publicly available content from:

- **Andrew Lock's "Exploring ASP.NET Core" Blog Series** -- Deep coverage of middleware authoring patterns, including
  IMiddleware vs convention-based trade-offs, pipeline ordering pitfalls, endpoint routing internals, and
  IExceptionHandler composition. Source: https://andrewlock.net/
- **Official ASP.NET Core Middleware Documentation** -- Middleware fundamentals, factory-based activation, and error
  handling patterns. Source: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/

> **Note:** This skill applies publicly documented guidance. It does not represent or speak for the named sources.

## References

- [ASP.NET Core middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/)
- [Write custom ASP.NET Core middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write)
- [Factory-based middleware activation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility)
- [Handle errors in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling)
- [IExceptionHandler in .NET 8](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling#iexceptionhandler)
- [Exploring ASP.NET Core (Andrew Lock)](https://andrewlock.net/)

---

## Attribution

Adapted from [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills) (MIT license).

Code Navigation (Serena MCP)

Primary approach: Use Serena symbol operations for efficient code navigation:

  1. Find definitions: serena_find_symbol instead of text search
  2. Understand structure: serena_get_symbols_overview for file organization
  3. Track references: serena_find_referencing_symbols for impact analysis
  4. Precise edits: serena_replace_symbol_body for clean modifications

When to use Serena vs traditional tools:

  • Use Serena: Navigation, refactoring, dependency analysis, precise edits
  • Use Read/Grep: Reading full files, pattern matching, simple text operations
  • Fallback: If Serena unavailable, traditional tools work fine

Example workflow:

# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"

# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Patterns and techniques for evaluating and improving AI agent outputs.

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

Comprehensive AI prompt engineering safety review and improvement prompt. Analyzes prompts for safety, bias, security vulnerabilities, and effectiveness while providing detailed improvement recommendations.

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

Use when user requests research requiring multiple sources, comprehensive analysis, or synthesis across topics - technical research, domain knowledge gathering, market analysis, or learning about complex subjects

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

AI-powered wiki generation for code repositories with commands, agents, and skills

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

Use when building .NET 10 or C# 14 applications; when using minimal APIs, modular monolith patterns, or feature folders; when implementing HTTP resilience, Options pattern, Channels, or validation; when seeing outdated patterns like old extension method syntax

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

Implements accessible .NET UI. SemanticProperties, ARIA, AutomationPeer, testing per platform.

The English summary is being prepared. Showing the original description.

rudironsoni/Synaxis2Updated Mar 17, 2026

See all skills by rudironsoni

Report a problem with this skill