Skip to main content

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

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 请求管道的核心构建块
  • 中间件按注册顺序执行,顺序很重要
  • 每个中间件可以选择是否调用下一个中间件
  • 使用 UseRunMap 方法创建内联中间件
  • 创建自定义中间件类以实现复杂逻辑
  • 中间件适用于整个应用的横切关注点
  • 过滤器更适合 MVC 特定的逻辑

相关资源