关于 middleware-patterns
middleware-patterns 是面向 ASP.NET Core 开发者的中间件模式技能,讲解请求管道顺序、分支与短路处理,以及中间件的实现、异常处理和测试。
主要功能
- 01理解管道顺序及其影响
- 02实现约定式、工厂式和内联中间件
- 03配置条件分支与独立管道
- 04处理异常及状态码响应
- 05测试中间件行为
适用场景
- 01安排路由、授权和 CORS 中间件
- 02添加请求日志或 API 密钥校验
- 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 常见注意事项
- 不要将
UseAuthorization()放在UseRouting()之前——授权需要端点元数据。 - 不要将
UseCors()放在UseAuthorization()之后——CORS 预检请求不包含身份验证令牌。 - 在透传中间件中不要忘记调用
next()——否则会在不知不觉中短路管道。 - 不要在未调用
EnableBuffering()的情况下读取Request.Body——默认情况下,请求正文只能向前读取。 - 未注册 DI 时不要注册
IMiddleware——需要显式调用services.AddScoped<T>()。 - 如果下游已开始响应,调用
next()后不要写入Response.Body——请检查context.Response.HasStarted。
参考资料
评论
支持 Markdown