前言

在前后端联调接口时,最痛苦的事情之一就是不同异常返回的格式不一致:业务异常、系统异常、模型验证错误都可能结构各异,前端处理成本高。传统做法通常是:

  1. 自定义中间件捕获异常
  2. 使用异常过滤器 (ExceptionFilter)

但这些方式存在两个问题:

  • 某些阶段抛出的异常(例如模型绑定 / 类型转换错误)可能无法被可靠捕获;
  • 需要手动维护额外的分层与侵入式代码。

从 .NET 8 开始,推荐的方式是使用内置的 IExceptionHandler 管道,这种方式更贴近框架设计,且更清晰集中。官方文档可参考:

处理 .NET 中的错误 - 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 格式。如果我们希望与全局异常输出保持一致,有两种策略:

  1. 禁用自动模型验证过滤器,改为手动/第三方验证(如 FluentValidation)。
  2. 保留 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 输出”这两种方式实现