全局异常处理中间件
大约 8 分钟约 2524 字
全局异常处理中间件
简介
全局异常处理是构建健壮 Web API 的关键环节。ASP.NET Core 提供了中间件管道机制,可以在请求处理的统一位置捕获和处理异常,避免异常信息泄露,提供一致的错误响应格式。掌握自定义异常类型、中间件模式和 ProblemDetails 规范,可以构建专业的错误处理体系。
特点
自定义异常类型
业务异常体系
/// <summary>
/// 自定义异常类型
/// </summary>
// 基础业务异常
public class BusinessException : Exception
{
public int StatusCode { get; }
public string Code { get; }
public BusinessException(string message, int statusCode = 400, string? code = null)
: base(message)
{
StatusCode = statusCode;
Code = code ?? $"BIZ_{statusCode}";
}
}
// 资源未找到
public class NotFoundException : BusinessException
{
public NotFoundException(string resource, object key)
: base($"{resource} ({key}) 不存在", 404, "NOT_FOUND") { }
}
// 验证失败
public class ValidationException : BusinessException
{
public IDictionary<string, string[]> Errors { get; }
public ValidationException(IDictionary<string, string[]> errors)
: base("请求参数验证失败", 422, "VALIDATION_ERROR")
{
Errors = errors;
}
}
// 权限不足
public class ForbiddenException : BusinessException
{
public ForbiddenException(string message = "你没有权限执行此操作")
: base(message, 403, "FORBIDDEN") { }
}全局异常中间件
中间件实现
/// <summary>
/// 全局异常处理中间件
/// </summary>
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)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var (statusCode, problemDetails) = exception switch
{
BusinessException biz => (biz.StatusCode, new ProblemDetails
{
Status = biz.StatusCode,
Title = "业务异常",
Detail = biz.Message,
Extensions = { ["code"] = biz.Code }
}),
ValidationException validation => (validation.StatusCode, new ProblemDetails
{
Status = validation.StatusCode,
Title = "参数验证失败",
Detail = validation.Message,
Extensions = { ["errors"] = validation.Errors }
}),
UnauthorizedAccessException => (401, new ProblemDetails
{
Status = 401,
Title = "未授权",
Detail = "请先登录"
}),
_ => (500, new ProblemDetails
{
Status = 500,
Title = "服务器内部错误",
Detail = "请求处理失败,请稍后重试"
})
};
// 记录日志
if (statusCode == 500)
{
_logger.LogError(exception, "未处理异常:{Message}", exception.Message);
}
else
{
_logger.LogWarning(exception, "业务异常:{Message}", exception.Message);
}
context.Response.StatusCode = statusCode;
context.Response.WriteAsJsonAsync(problemDetails);
}
}
// 注册中间件
app.UseMiddleware<ExceptionHandlingMiddleware>();ProblemDetails
标准错误响应
/// <summary>
/// ProblemDetails 配置
/// </summary>
// 注册 ProblemDetails 服务
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Instance = ctx.HttpContext.Request.Path;
ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier;
ctx.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
};
});
// 在 Minimal API 中使用
app.MapGet("/api/users/{id}", async (int id, IUserService service) =>
{
var user = await service.GetByIdAsync(id);
return user is null
? Results.NotFound(new ProblemDetails
{
Status = 404,
Title = "用户不存在",
Detail = $"ID 为 {id} 的用户不存在"
})
: Results.Ok(user);
});
// 在 Controller 中使用
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var user = await _userService.GetByIdAsync(id);
if (user == null) return NotFound();
return user;
}
// ModelState 自动验证(Controller)
[ApiController]
public class UsersController : ControllerBase
{
[HttpPost]
public async Task<ActionResult<User>> Create(CreateUserRequest request)
{
// ModelState 无效时自动返回 400 ProblemDetails
var user = await _userService.CreateAsync(request);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
}异常过滤器
IExceptionFilter
/// <summary>
/// MVC 异常过滤器
/// </summary>
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
context.Result = context.Exception switch
{
NotFoundException => new NotFoundObjectResult(new
{
code = "NOT_FOUND",
message = context.Exception.Message
}),
ValidationException ve => new UnprocessableEntityObjectResult(new
{
code = "VALIDATION_ERROR",
message = ve.Message,
errors = ve.Errors
}),
BusinessException be => new ObjectResult(new
{
code = be.Code,
message = be.Message
}) { StatusCode = be.StatusCode },
_ => new ObjectResult(new
{
code = "INTERNAL_ERROR",
message = "服务器内部错误"
}) { StatusCode = 500 }
};
_logger.LogError(context.Exception, "全局异常:{Message}", context.Exception.Message);
context.ExceptionHandled = true;
}
}
// 注册
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});Minimal API 全局异常处理
// Minimal API 中的异常处理方案
// 方式1:全局异常处理中间件(推荐)
var app = builder.Build();
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (BusinessException ex)
{
context.Response.StatusCode = ex.StatusCode;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = ex.StatusCode,
Title = "业务异常",
Detail = ex.Message,
Extensions = { ["code"] = ex.Code }
});
}
catch (ValidationException ex)
{
context.Response.StatusCode = 422;
await context.Response.WriteAsJsonAsync(new ValidationProblemDetails(ex.Errors)
{
Status = 422,
Title = "参数验证失败",
Detail = ex.Message
});
}
catch (Exception ex)
{
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger("GlobalException");
logger.LogError(ex, "未处理异常: {Message}", ex.Message);
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = 500,
Title = "服务器内部错误",
Detail = "请求处理失败,请稍后重试",
Extensions = { ["traceId"] = context.TraceIdentifier }
});
}
});
// 方式2:使用 Results.Problem
app.MapGet("/api/users/{id}", async (int id, IUserService service) =>
{
try
{
var user = await service.GetByIdAsync(id);
return user is null
? Results.NotFound(ProblemDetailsFactory.CreateNotFound(id))
: Results.Ok(user);
}
catch (NotFoundException)
{
return Results.NotFound();
}
});
// 方式3:Endpoint Filter(局部异常处理)
app.MapPost("/api/orders", async (CreateOrderRequest request, IOrderService service) =>
{
return Results.Created($"/api/orders/{request.Id}", await service.CreateAsync(request));
})
.AddEndpointFilter(async (context, next) =>
{
try
{
return await next(context);
}
catch (ValidationException ex)
{
return Results.ValidationProblem(ex.Errors);
}
});异常处理中间件 vs 异常过滤器
中间件(Middleware) 异常过滤器(Exception Filter)
------------------------------------------- -------------------------------------------
捕获所有异常(包括中间件、路由、终结点) 只捕获 MVC/API Controller 中的异常
不依赖 MVC 框架 需要 AddControllers()
适合 Minimal API 和混合项目 只在 MVC/API 管道中生效
无法访问 Controller 上下文 可以访问 ActionDescriptor、ModelState
需要手动处理响应格式 框架自动处理响应
推荐方案:
- Minimal API 项目 → 使用异常处理中间件
- MVC/API Controller 项目 → 使用异常过滤器 + 中间件
- 混合项目 → 两者都配置,中间件作为兜底第三方集成异常处理
// 数据库异常转换
public class DbExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<DbExceptionMiddleware> _logger;
public DbExceptionMiddleware(RequestDelegate next, ILogger<DbExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "数据库更新失败");
var statusCode = ex.InnerException switch
{
SqlException sql when sql.Number == 2627 => 409, // 唯一约束冲突
SqlException sql when sql.Number == 547 => 409, // 外键约束冲突
SqlException sql when sql.Number == 2601 => 409, // 唯一索引冲突
TimeoutException => 504, // 查询超时
_ => 500
};
var message = ex.InnerException?.Message ?? "数据库操作失败";
var code = statusCode == 409 ? "CONFLICT" : "DATABASE_ERROR";
context.Response.StatusCode = statusCode;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = statusCode,
Title = code,
Detail = message,
Extensions = { ["traceId"] = context.TraceIdentifier }
});
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "外部服务调用失败: {StatusCode}", ex.StatusCode);
var statusCode = (int)(ex.StatusCode ?? HttpStatusCode.InternalServerError);
context.Response.StatusCode = 502; // Bad Gateway
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = 502,
Title = "外部服务异常",
Detail = "依赖的第三方服务暂时不可用",
Extensions = { ["traceId"] = context.TraceIdentifier }
});
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogWarning(ex, "请求超时");
context.Response.StatusCode = 504;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = 504,
Title = "请求超时",
Detail = "服务处理超时,请稍后重试",
Extensions = { ["traceId"] = context.TraceIdentifier }
});
}
catch (OperationCanceledException)
{
context.Response.StatusCode = 499;
await context.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = 499,
Title = "请求已取消",
Detail = "客户端取消了请求"
});
}
}
}异常处理测试
/// <summary>
/// 异常处理集成测试
/// </summary>
public class ErrorHandlingTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public ErrorHandlingTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task NotFound_ReturnsProblemDetails()
{
var response = await _client.GetAsync("/api/users/99999");
Assert.Equal(404, (int)response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.NotNull(problem);
Assert.Equal(404, problem.Status);
Assert.NotEmpty(problem.Title);
}
[Fact]
public async Task ValidationError_Returns422WithErrors()
{
var response = await _client.PostAsJsonAsync("/api/users", new
{
name = "", // 名称不能为空
email = "invalid" // 邮箱格式不正确
});
Assert.Equal(422, (int)response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
Assert.NotNull(problem);
Assert.NotNull(problem.Errors);
Assert.True(problem.Errors.Count > 0);
}
[Fact]
public async Task UnhandledException_Returns500WithTraceId()
{
var response = await _client.GetAsync("/api/test/error");
Assert.Equal(500, (int)response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.NotNull(problem);
Assert.Equal(500, problem.Status);
Assert.True(problem.Extensions.ContainsKey("traceId"));
}
[Fact]
public async Task ProblemDetails_ContainsRequiredFields()
{
var response = await _client.GetAsync("/api/test/notfound");
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.NotNull(problem);
Assert.True(problem.Extensions.ContainsKey("traceId"));
Assert.True(problem.Extensions.ContainsKey("timestamp"));
Assert.NotEmpty(problem.Instance?.ToString() ?? "");
}
}优点
缺点
总结
全局异常处理推荐使用中间件模式,配合自定义异常类型实现分层处理。错误响应格式统一使用 ProblemDetails(RFC 7807)。业务异常(NotFoundException、ValidationException)与系统异常分离处理。中间件注册要在路由中间件之前,确保所有请求都能被捕获。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《全局异常处理中间件》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《全局异常处理中间件》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《全局异常处理中间件》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《全局异常处理中间件》最大的收益和代价分别是什么?
