mcpField
M
middleware-patterns

middleware-patterns

github.com/wshaddix开发8677 次安装35 周前更新
代码仓库

关于 middleware-patterns

middleware-patterns 是面向 ASP.NET Core 开发者的中间件模式技能,讲解请求管道顺序、分支与短路处理,以及中间件的实现、异常处理和测试。

主要功能

  1. 01理解管道顺序及其影响
  2. 02实现约定式、工厂式和内联中间件
  3. 03配置条件分支与独立管道
  4. 04处理异常及状态码响应
  5. 05测试中间件行为

适用场景

  1. 01安排路由、授权和 CORS 中间件
  2. 02添加请求日志或 API 密钥校验
  3. 03为不同路径或环境配置管道

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill middleware-patterns

安装后在 Claude Code 中输入/middleware-patterns确认已加载。

详细介绍

ASP.NET Core 中间件模式

原理

中间件是 ASP.NET Core 请求处理的骨干。设计得当的中间件能够实现日志记录、身份验证和缓存等横切关注点。了解管道顺序和中间件模式对于构建稳健的应用至关重要。


管道顺序

中间件按注册顺序执行。顺序至关重要——将中间件放在错误的位置会导致难以察觉的错误。

推荐顺序
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();
顺序为何重要
错误后果
UseAuthorization() 在 UseRouting() 之前授权没有终结点元数据——所有请求都会通过
UseCors() 在 UseAuthorization() 之后预检请求因缺少身份验证令牌而失败
UseExceptionHandler() 在自定义中间件之后自定义中间件中的异常不会得到处理
UseStaticFiles() 在 UseAuthorization() 之后静态文件会不必要地要求身份验证

模式 1:基于约定的中间件

基于约定的中间件使用带有 RequestDelegate 的构造函数和 InvokeAsync 方法。

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();

模式 2:基于工厂的中间件(IMiddleware)

对于需要作用域服务的中间件,请实现 IMiddleware。它使用 DI 为每个请求创建中间件实例:

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>();
基于约定的中间件与 IMiddleware
方面基于约定IMiddleware
生命周期单例(创建一次)每个请求一个(来自 DI)
作用域服务仅通过 InvokeAsync 参数注入通过构造函数注入
注册仅需 UseMiddleware<T>()需要 services.Add*<T>() + UseMiddleware<T>()
性能稍快每个请求都从 DI 解析

模式 3:内联中间件

适用于简单的一次性逻辑:

app.Use——传递处理
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Request-Id"] = context.TraceIdentifier;
    await next(context);
});
app.Run——终止处理
app.Run(async context =>
{
    await context.Response.WriteAsync("Fallback response");
});
app.Map——按路径分支
app.Map("/api/diagnostics", diagnosticApp =>
{
    diagnosticApp.Run(async context =>
    {
        var data = new
        {
            MachineName = Environment.MachineName,
            Timestamp = DateTimeOffset.UtcNow
        };
        await context.Response.WriteAsJsonAsync(data);
    });
});

模式 4:短路逻辑

中间件可以不调用 next(),从而使管道短路。

请求验证
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);
    }
}
功能标志门控
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);
        });
    });

模式 5:请求与响应处理

读取请求正文
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);
    }
}
修改响应
public async Task InvokeAsync(HttpContext context)
{
    var originalBodyStream = context.Response.Body;

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

    await _next(context);

    context.Response.Body.Seek(0, SeekOrigin.Begin);
    var responseText = await new StreamReader(context.Response.Body).ReadToEndAsync();
    context.Response.Body.Seek(0, SeekOrigin.Begin);

    await responseBody.CopyToAsync(originalBodyStream);
}

注意: 替换响应正文会增加内存开销。仅用于诊断。


模式 6:异常处理中间件

内置异常处理程序
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
        });
    });
});
IExceptionHandler(.NET 8+)

可以注册多个处理程序,它们会按顺序调用:

public sealed class ValidationExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken ct)
    {
        if (exception is not ValidationException validationException)
            return false;

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

        return true;
    }
}

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

builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

app.UseExceptionHandler();
用于处理非异常错误的 StatusCodePages
app.UseStatusCodePagesWithReExecute("/error/{0}");

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

模式 7:条件中间件

UseWhen——条件分支(重新汇入管道)
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    apiApp =>
    {
        apiApp.UseRateLimiter();
    });
MapWhen——条件分支(不会重新汇入管道)
app.MapWhen(
    context => context.WebSockets.IsWebSocketRequest,
    wsApp =>
    {
        wsApp.Run(async context =>
        {
            using var ws = await context.WebSockets.AcceptWebSocketAsync();
        });
    });
特定环境的中间件
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

模式 8:分支中间件

为不同的路由前缀创建完全独立的管道:

app.Map("/api", apiApp =>
{
    apiApp.UseExceptionHandler("/api/error");
    apiApp.UseHttpsRedirection();
    apiApp.UseAuthentication();
    apiApp.UseAuthorization();
    apiApp.UseRateLimiter();
    apiApp.MapControllers();
});

app.Map("/webhooks", webhookApp =>
{
    webhookApp.UseMiddleware<WebhookSignatureValidation>();
    webhookApp.UseMiddleware<WebhookIdempotency>();
    webhookApp.MapRazorPages();
});

app.UseExceptionHandler("/Error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();

模式 9:带选项的中间件

public class RateLimitingMiddlewareOptions
{
    public int MaxRequestsPerSecond { get; set; } = 10;
    public int BurstSize { get; set; } = 20;
    public TimeSpan BlockDuration { get; set; } = TimeSpan.FromMinutes(1);
}

public class RateLimitingMiddleware(
    RequestDelegate next,
    IOptions<RateLimitingMiddlewareOptions> options,
    IMemoryCache cache)
{
    private readonly RateLimitingMiddlewareOptions _options = options.Value;

    public async Task Invoke(HttpContext context)
    {
        var clientId = GetClientIdentifier(context);
        var cacheKey = $"ratelimit:{clientId}";

        if (!await TryAcquireTokenAsync(cacheKey))
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers.RetryAfter = _options.BlockDuration.TotalSeconds.ToString();
            await context.Response.WriteAsync("Rate limit exceeded");
            return;
        }

        await next(context);
    }

    private string GetClientIdentifier(HttpContext context)
    {
        return context.User.Identity?.Name ??
               context.Connection.RemoteIpAddress?.ToString() ??
               "anonymous";
    }

    private async Task<bool> TryAcquireTokenAsync(string cacheKey) => true;
}

builder.Services.Configure<RateLimitingMiddlewareOptions>(options =>
{
    options.MaxRequestsPerSecond = 5;
    options.BurstSize = 10;
});

app.UseMiddleware<RateLimitingMiddleware>();

模式 10:中间件测试

public class MiddlewareTests
{
    [Fact]
    public async Task SecurityHeadersMiddleware_AddsRequiredHeaders()
    {
        var middleware = new SecurityHeadersMiddleware(async (context) =>
        {
            await Task.CompletedTask;
        });

        var context = new DefaultHttpContext();

        await middleware.Invoke(context);

        Assert.Equal("nosniff", context.Response.Headers["X-Content-Type-Options"].ToString());
        Assert.Equal("DENY", context.Response.Headers["X-Frame-Options"].ToString());
    }

    [Fact]
    public async Task ApiKeyMiddleware_Returns401_WhenKeyMissing()
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new[] { new KeyValuePair<string, string?>("ApiKey", "test-key") })
            .Build();

        var middleware = new ApiKeyMiddleware(async (context) =>
        {
            await Task.CompletedTask;
        }, config);

        var context = new DefaultHttpContext();
        context.Response.Body = new MemoryStream();

        await middleware.Invoke(context);

        Assert.Equal(401, context.Response.StatusCode);
    }
}

反模式

响应开始后仍调用 Next
// BAD: Calling next after response has started
public async Task Invoke(HttpContext context)
{
    await context.Response.WriteAsync("Before");
    await next(context); // May fail
    await context.Response.WriteAsync("After"); // Won't work
}

// GOOD: Only modify response before calling next
public async Task Invoke(HttpContext context)
{
    var originalBody = context.Response.Body;
    context.Response.Body = new MemoryStream();

    await next(context);

    context.Response.Body.Position = 0;
    await context.Response.Body.CopyToAsync(originalBody);
}
未还原上下文
// BAD: Not restoring HttpContext state
public async Task Invoke(HttpContext context)
{
    var originalUser = context.User;
    context.User = new ClaimsPrincipal();
    await next(context);
    // Missing: context.User = originalUser;
}

// GOOD: Always restore state
public async Task Invoke(HttpContext context)
{
    var originalUser = context.User;
    try
    {
        context.User = new ClaimsPrincipal();
        await next(context);
    }
    finally
    {
        context.User = originalUser;
    }
}

核心原则

  • 顺序决定一切——请求按从上到下的顺序执行中间件,响应则按从下到上的顺序执行
  • 异常处理程序应放在最前面——UseExceptionHandler 必须位于最外层
  • 可复用的中间件优先使用类,而非内联逻辑——易于测试、组合,并遵循单一职责原则
  • 作用域依赖项使用 IMiddleware——基于约定的中间件是单例
  • 有意触发短路——始终说明中间件不调用 next() 的原因
  • 避免在高频路径中处理响应正文——每个请求的内存使用量会翻倍

Agent 常见注意事项

  1. 不要将 UseAuthorization() 放在 UseRouting() 之前——授权需要端点元数据。
  2. 不要将 UseCors() 放在 UseAuthorization() 之后——CORS 预检请求不包含身份验证令牌。
  3. 在透传中间件中不要忘记调用 next()——否则会在不知不觉中短路管道。
  4. 不要在未调用 EnableBuffering() 的情况下读取 Request.Body——默认情况下,请求正文只能向前读取。
  5. 未注册 DI 时不要注册 IMiddleware——需要显式调用 services.AddScoped<T>()。
  6. 如果下游已开始响应,调用 next() 后不要写入 Response.Body——请检查 context.Response.HasStarted。

参考资料

评论

支持 Markdown