본문으로 건너뛰기
cccskills
무료GitHub에 공개

dotnet-middleware-patterns

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md20.3 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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"

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Patterns and techniques for evaluating and improving AI agent outputs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

deep-wiki

무료

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

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

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

rudironsoni의 스킬 모두 보기

이 스킬의 문제 신고