ASP.NET Core 中间件
什么是中间件?
核心概念
中间件是一种装配到应用管道中以处理请求和响应的组件。每个中间件:
- 可以选择是否将请求传递到管道中的下一个组件
- 可以在管道中的下一个组件之前和之后执行工作
中间件管道
graph LR
A[请求] --> B[中间件1]
B --> C[中间件2]
C --> D[中间件3]
D --> E[终结点]
E --> D
D --> C
C --> B
B --> F[响应]
执行顺序特点
- 顺序执行:请求从第一个中间件开始,依次经过每个中间件
- 双向处理:每个中间件可以在调用下一个中间件之前和之后执行代码
- 短路机制:任何中间件都可以选择不调用下一个中间件,直接返回响应
内置中间件
常用中间件配置顺序
var app = builder.Build();
// 1. 异常处理(应该最先)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// 2. HTTPS 重定向
app.UseHttpsRedirection();
// 3. 静态文件
app.UseStaticFiles();
// 4. 路由
app.UseRouting();
// 5. 跨域(CORS)
app.UseCors("AllowAll");
// 6. 认证
app.UseAuthentication();
// 7. 授权
app.UseAuthorization();
// 8. 响应压缩
app.UseResponseCompression();
// 9. 响应缓存
app.UseResponseCaching();
// 10. 终结点
app.MapControllers();
app.Run();
自定义中间件
1. 使用委托创建中间件
- 内联中间件
- 终端中间件
- 分支中间件
app.Use(async (context, next) =>
{
// 请求处理前
Console.WriteLine($"请求: {context.Request.Path}");
await next(); // 调用下一个中间件
// 响应处理后
Console.WriteLine($"响应: {context.Response.StatusCode}");
});
// Run 方法创建的中间件不会调用下一个中间件
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from terminal middleware!");
});
// Map 根据请求路径创建分支
app.Map("/api", apiApp =>
{
apiApp.Use(async (context, next) =>
{
Console.WriteLine("API 分支");
await next();
});
apiApp.Run(async context =>
{
await context.Response.WriteAsync("API Response");
});
});
// MapWhen 根据条件创建分支
app.MapWhen(
context => context.Request.Query.ContainsKey("debug"),
debugApp =>
{
debugApp.Run(async context =>
{
await context.Response.WriteAsync("Debug mode");
});
});
2. 基于约定的中间件
// RequestLoggingMiddleware.cs
public 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)
{
// 请求开始
var startTime = DateTime.UtcNow;
var requestPath = context.Request.Path;
var requestMethod = context.Request.Method;
_logger.LogInformation(
"开始处理请求 {Method} {Path}",
requestMethod,
requestPath);
try
{
// 调用下一个中间件
await _next(context);
}
finally
{
// 请求结束
var duration = DateTime.UtcNow - startTime;
_logger.LogInformation(
"完成请求 {Method} {Path} - 状态码: {StatusCode} - 耗时: {Duration}ms",
requestMethod,
requestPath,
context.Response.StatusCode,
duration.TotalMilliseconds);
}
}
}
// 扩展方法
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// 使用
app.UseRequestLogging();
3. 基于工厂的中间件
public class RequestCultureMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
await next(context);
}
}
// 注册为服务
builder.Services.AddTransient<RequestCultureMiddleware>();
// 使用
app.UseMiddleware<RequestCultureMiddleware>();
实用中间件示例
1. 异常处理中间件
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "发生未处理的异常");
await HandleExceptionAsync(context, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
var response = exception switch
{
ApplicationException _ => new
{
StatusCode = StatusCodes.Status400BadRequest,
Message = "应用程序错误",
Detailed = exception.Message
},
KeyNotFoundException _ => new
{
StatusCode = StatusCodes.Status404NotFound,
Message = "资源未找到",
Detailed = exception.Message
},
UnauthorizedAccessException _ => new
{
StatusCode = StatusCodes.Status401Unauthorized,
Message = "未授权访问",
Detailed = exception.Message
},
_ => new
{
StatusCode = StatusCodes.Status500InternalServerError,
Message = "服务器内部错误",
Detailed = exception.Message
}
};
context.Response.StatusCode = response.StatusCode;
await context.Response.WriteAsJsonAsync(response);
}
}
2. 请求限流中间件
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly IMemoryCache _cache;
private readonly int _requestLimit;
private readonly TimeSpan _timeWindow;
public RateLimitingMiddleware(
RequestDelegate next,
IMemoryCache cache,
int requestLimit = 100,
int timeWindowSeconds = 60)
{
_next = next;
_cache = cache;
_requestLimit = requestLimit;
_timeWindow = TimeSpan.FromSeconds(timeWindowSeconds);
}
public async Task InvokeAsync(HttpContext context)
{
var clientId = GetClientIdentifier(context);
var cacheKey = $"RateLimit_{clientId}";
if (!_cache.TryGetValue(cacheKey, out int requestCount))
{
requestCount = 0;
}
if (requestCount >= _requestLimit)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsJsonAsync(new
{
Message = "请求过于频繁,请稍后再试",
RetryAfter = _timeWindow.TotalSeconds
});
return;
}
// 增加计数
_cache.Set(cacheKey, requestCount + 1, _timeWindow);
// 添加响应头
context.Response.Headers["X-RateLimit-Limit"] = _requestLimit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = (_requestLimit - requestCount - 1).ToString();
await _next(context);
}
private string GetClientIdentifier(HttpContext context)
{
// 优先使用认证用户 ID
if (context.User?.Identity?.IsAuthenticated == true)
{
return context.User.Identity.Name ?? "anonymous";
}
// 否则使用 IP 地址
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
3. API 密钥验证中间件
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string ApiKeyHeaderName = "X-API-Key";
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
// 跳过不需要验证的路径
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
if (!context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new
{
Message = "缺少 API 密钥"
});
return;
}
var validApiKey = _configuration["ApiKey"];
if (!validApiKey.Equals(extractedApiKey))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new
{
Message = "无效的 API 密钥"
});
return;
}
await _next(context);
}
}
4. 请求/响应日志中间件
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
public RequestResponseLoggingMiddleware(
RequestDelegate next,
ILogger<RequestResponseLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 记录请求
await LogRequest(context);
// 保存原始响应流
var originalBodyStream = context.Response.Body;
try
{
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
// 调用下一个中间件
await _next(context);
// 记录响应
await LogResponse(context);
// 复制响应到原始流
await responseBody.CopyToAsync(originalBodyStream);
}
finally
{
context.Response.Body = originalBodyStream;
}
}
private async Task LogRequest(HttpContext context)
{
context.Request.EnableBuffering();
var requestBody = string.Empty;
if (context.Request.ContentLength > 0)
{
using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
leaveOpen: true);
requestBody = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
}
_logger.LogInformation(
"HTTP Request: {Method} {Path} {QueryString}\nHeaders: {Headers}\nBody: {Body}",
context.Request.Method,
context.Request.Path,
context.Request.QueryString,
string.Join(", ", context.Request.Headers.Select(h => $"{h.Key}={h.Value}")),
requestBody);
}
private async Task LogResponse(HttpContext context)
{
context.Response.Body.Seek(0, SeekOrigin.Begin);
var responseBody = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
_logger.LogInformation(
"HTTP Response: {StatusCode}\nHeaders: {Headers}\nBody: {Body}",
context.Response.StatusCode,
string.Join(", ", context.Response.Headers.Select(h => $"{h.Key}={h.Value}")),
responseBody);
}
}
5. 健康检查中间件
public class HealthCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<HealthCheckMiddleware> _logger;
public HealthCheckMiddleware(
RequestDelegate next,
ILogger<HealthCheckMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path == "/health")
{
var health = await CheckHealthAsync();
context.Response.ContentType = "application/json";
context.Response.StatusCode = health.IsHealthy
? StatusCodes.Status200OK
: StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsJsonAsync(health);
return;
}
await _next(context);
}
private async Task<HealthStatus> CheckHealthAsync()
{
var checks = new List<HealthCheck>
{
await CheckDatabaseAsync(),
await CheckCacheAsync(),
await CheckExternalApiAsync()
};
var isHealthy = checks.All(c => c.IsHealthy);
return new HealthStatus
{
IsHealthy = isHealthy,
Timestamp = DateTime.UtcNow,
Checks = checks
};
}
private async Task<HealthCheck> CheckDatabaseAsync()
{
try
{
// 模拟数据库检查
await Task.Delay(10);
return new HealthCheck
{
Name = "Database",
IsHealthy = true,
ResponseTime = 10
};
}
catch (Exception ex)
{
_logger.LogError(ex, "数据库健康检查失败");
return new HealthCheck
{
Name = "Database",
IsHealthy = false,
Message = ex.Message
};
}
}
private async Task<HealthCheck> CheckCacheAsync()
{
try
{
await Task.Delay(5);
return new HealthCheck
{
Name = "Cache",
IsHealthy = true,
ResponseTime = 5
};
}
catch (Exception ex)
{
return new HealthCheck
{
Name = "Cache",
IsHealthy = false,
Message = ex.Message
};
}
}
private async Task<HealthCheck> CheckExternalApiAsync()
{
try
{
await Task.Delay(20);
return new HealthCheck
{
Name = "ExternalAPI",
IsHealthy = true,
ResponseTime = 20
};
}
catch (Exception ex)
{
return new HealthCheck
{
Name = "ExternalAPI",
IsHealthy = false,
Message = ex.Message
};
}
}
}
public class HealthStatus
{
public bool IsHealthy { get; set; }
public DateTime Timestamp { get; set; }
public List<HealthCheck> Checks { get; set; } = new();
}
public class HealthCheck
{
public string Name { get; set; } = string.Empty;
public bool IsHealthy { get; set; }
public string? Message { get; set; }
public long ResponseTime { get; set; }
}
中间件最佳实践
最佳实践
1. 顺序很重要
// ✅ 正确:异常处理在最前面
app.UseExceptionHandler("/Error");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// ❌ 错误:异常处理在后面无法捕获前面的异常
app.UseAuthentication();
app.UseAuthorization();
app.UseExceptionHandler("/Error");
app.MapControllers();
2. 避免阻塞操作
// ❌ 差:同步阻塞
app.Use((context, next) =>
{
Thread.Sleep(1000); // 阻塞线程
return next();
});
// ✅ 好:使用异步
app.Use(async (context, next) =>
{
await Task.Delay(1000); // 不阻塞线程
await next();
});
3. 条件性应用中间件
// 只在生产环境启用 HSTS
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
// 只对特定路径应用中间件
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
appBuilder =>
{
appBuilder.UseMiddleware<ApiKeyMiddleware>();
});
4. 性能考虑
// 避免在每个请求中进行昂贵的操作
// ❌ 差
app.Use(async (context, next) =>
{
var config = new ConfigurationBuilder() // 每次请求都创建
.AddJsonFile("appsettings.json")
.Build();
await next();
});
// ✅ 好:通过依赖注入获取单例服务
public class MyMiddleware
{
private readonly IConfiguration _configuration;
public MyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_configuration = configuration; // 注入的单例
}
}
5. 错误处理
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "中间件发生错误");
// 防止响应已经开始后再写入
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Internal Server Error");
}
}
}
中间件 vs 过滤器
特性 | 中间件 | 过滤器 |
---|---|---|
作用范围 | 整个应用管道 | MVC/API 控制器和操作 |
访问路由数据 | 有限(UseRouting 之后) | 完全访问 |
访问 MVC 上下文 | 否 | 是 |
执行顺序 | 按注册顺序 | 按类型和范围 |
适用场景 | 通用 HTTP 处理 | MVC 特定逻辑 |
性能 | 更高效 | 稍慢 |
使用建议
- 使用中间件:日志、异常处理、认证、CORS、静态文件
- 使用过滤器:模型验证、操作日志、结果转换、缓存
测试中间件
public class RequestLoggingMiddlewareTests
{
[Fact]
public async Task InvokeAsync_LogsRequestInformation()
{
// Arrange
var loggerMock = new Mock<ILogger<RequestLoggingMiddleware>>();
var context = new DefaultHttpContext();
context.Request.Method = "GET";
context.Request.Path = "/test";
var middleware = new RequestLoggingMiddleware(
(innerContext) => Task.CompletedTask,
loggerMock.Object);
// Act
await middleware.InvokeAsync(context);
// Assert
loggerMock.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("GET")),
null,
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.AtLeastOnce);
}
}
总结
关键要点
- 中间件是 ASP.NET Core 请求管道的核心构建块
- 中间件按注册顺序执行,顺序很重要
- 每个中间件可以选择是否调用下一个中间件
- 使用
Use
、Run
、Map
方法创建内联中间件 - 创建自定义中间件类以实现复杂逻辑
- 中间件适用于整个应用的横切关注点
- 过滤器更适合 MVC 特定的逻辑