前言
在前后端联调接口时,最痛苦的事情之一就是不同异常返回的格式不一致:业务异常、系统异常、模型验证错误都可能结构各异,前端处理成本高。传统做法通常是:
- 自定义中间件捕获异常
- 使用异常过滤器 (
ExceptionFilter)
但这些方式存在两个问题:
- 某些阶段抛出的异常(例如模型绑定 / 类型转换错误)可能无法被可靠捕获;
- 需要手动维护额外的分层与侵入式代码。
从 .NET 8 开始,推荐的方式是使用内置的 IExceptionHandler 管道,这种方式更贴近框架设计,且更清晰集中。官方文档可参考:
下面演示一个统一异常处理的实际落地方案,并扩展到模型验证场景。
IExceptionHandler 的实现与示例
1. 定义统一结果模型(示例)
假设我们有一个通用的 API 返回模型:
public class ApiResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int StatusCode { get; set; }
public object? Data { get; set; }
}
以及一个业务友好型异常:
public class FriendlyException : Exception
{
public FriendlyException(string message) : base(message) { }
}
2. 实现自定义异常处理器
通过实现 IExceptionHandler 接口集中处理:
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) => _logger = logger;
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken token)
{
var (statusCode, message) = exception switch
{
FriendlyException friendly => (StatusCodes.Status400BadRequest, friendly.Message),
_ => (StatusCodes.Status500InternalServerError, exception.Message)
};
var apiResult = new ApiResult
{
Success = false,
Message = message,
StatusCode = statusCode
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsJsonAsync(apiResult, cancellationToken: token);
return true; // 表示此异常已被处理
}
}
FriendlyException 是自定义的一个业务友好型异常类型,通过模式匹配:
var (statusCode, message) = exception switch
{
FriendlyException badReqEx => (StatusCodes.Status400BadRequest, badReqEx.Message),
_ => (StatusCodes.Status500InternalServerError, exception.Message)
};
来区分异常类型并返回不同的 HTTP 状态码与消息。
3. 在 Program.cs 中注册
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
// builder.Services.AddProblemDetails(); // 可选:用于标准化 ProblemDetails 输出
var app = builder.Build();
// 注意:如果不使用AddProblemDetails的话必须需要传入 options,否则某些版本会启动报错
app.UseExceptionHandler(options => { });
4. 验证基本异常捕获
app.MapGet("/friendlyException", () =>
{
throw new FriendlyException("This is a friendly exception message.");
});
app.MapGet("/generalException", () =>
{
throw new Exception("This is a general exception.");
});
响应示例:
{
"success": false,
"message": "This is a general exception.",
"data": null,
"statusCode": 500
}
{
"success": false,
"message": "This is a friendly exception message.",
"data": null,
"statusCode": 400
}
5. 扩展:模型验证异常处理
模型验证错误(Model Validation Errors)默认由框架拦截并返回 ProblemDetails 格式。如果我们希望与全局异常输出保持一致,有两种策略:
- 禁用自动模型验证过滤器,改为手动/第三方验证(如 FluentValidation)。
- 保留
ApiController特性,但通过InvalidModelStateResponseFactory自定义输出。
下面示例选用第二种方式保持其它功能(例如路由推断、多内容类型绑定)不受影响。
[ApiController]
[Route("/")]
public class DemoController : ControllerBase
{
[HttpPost("modelException")]
public IActionResult ModelException([FromBody] WeatherForecast input)
{
Console.WriteLine($"Received date: {input.Date}");
throw new Exception("This is a model exception.");
}
}
// 执行结果如下:
// {
// "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
// "title": "One or more validation errors occurred.",
// "status": 400,
// "errors": {
// "Date": [
// "date is require"
// ]
// },
// "traceId": "00-7daf95703e8c8e7793e437df69f44562-f99a216f6fbcd8a7-00"
// }
默认返回:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.
",
"status": 400,
"errors": { "Date": [ "date is require" ] }
}
如果希望完全接入统一格式,可用以下两种方式:
方案 A:禁用自动模型验证(需要自行验证)
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
这种方式需要配合手动 ModelState 检查或 FluentValidation。
方案 B:自定义 InvalidModelState 响应
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value is { Errors.Count: > 0 })
.SelectMany(e => e.Value!.Errors.Select(err => e.Key + ": " + err.ErrorMessage));
var apiResult = new ApiResult
{
Success = false,
Message = "参数校验失败",
StatusCode = StatusCodes.Status400BadRequest,
Data = string.Join("; ", errors)
};
return new BadRequestObjectResult(apiResult);
};
});
// 测试结果如下:
// {
// "success": false,
// "message": "参数校验失败",
// "data": "Date: date is require",
// "statusCode": 400
// }
这样即保留 ApiController 带来的其它便利,又能统一前端的错误处理逻辑。
结语
使用 IExceptionHandler 能在 .NET 中以更低侵入的方式集中管理异常,避免遗漏类型转换 / 模型绑定阶段的错误,并输出统一结构,降低前端解析复杂度。对于模型验证错误,可以用“禁用自动验证 + 第三方验证”或“自定义 InvalidModelState 输出”这两种方式实现