
在ASP.NET Core Web API项目中实现全局异常处理与自定义错误响应机制
你好,我是源码库的博主。在构建健壮的ASP.NET Core Web API时,异常处理是保障系统稳定性和提供友好客户端体验的关键一环。你是否曾为API突然抛出一个满是堆栈信息的500错误而头疼?或者希望所有异常都能以统一、结构化的JSON格式返回给前端?今天,我就结合自己的实战经验,带你一步步构建一个优雅的全局异常处理与自定义错误响应机制,并分享几个我踩过的“坑”。
一、为什么需要全局异常处理?
在项目初期,我们可能习惯在Controller的Action里写一堆try-catch。但随着项目膨胀,这种方式不仅重复代码多,还容易遗漏。更糟糕的是,一些未处理的异常(如数据库连接失败、空引用)会直接导致HTTP 500,返回的可能是敏感的堆栈信息,既不安全也不友好。
全局异常处理的核心目标就两个:1. 集中管理所有未处理异常;2. 统一响应格式。无论哪里出错,客户端收到的都是一个结构一致的错误对象,包含错误码、消息和(在开发环境下)的详细信息,这极大简化了前端的错误处理逻辑。
二、构建自定义错误响应模型
首先,我们定义一个标准的API错误响应类。这是与前端约定的“契约”。
// Models/ApiResponse.cs
namespace YourProject.Models
{
public class ApiResponse
{
public bool Success { get; set; }
public T? Data { get; set; }
public string? Message { get; set; }
public string? ErrorCode { get; set; } // 可选,用于更细粒度的错误分类
}
// 专门用于错误响应的非泛型版本
public class ApiErrorResponse : ApiResponse
这个模型很灵活,ErrorCode可以用来定义业务错误码,Detail只在开发环境暴露,Errors则专门用来承载模型验证错误。
三、创建自定义异常与异常过滤器
对于业务逻辑异常(如“用户不存在”、“余额不足”),我们不应该抛出普通的Exception。创建一个自定义业务异常类会更好管理。
// Exceptions/BusinessException.cs
namespace YourProject.Exceptions
{
public class BusinessException : Exception
{
public string ErrorCode { get; }
public BusinessException(string message, string errorCode = "BUSINESS_ERROR") : base(message)
{
ErrorCode = errorCode;
}
}
}
接下来是核心部分:全局异常过滤器。我们将实现IExceptionFilter接口。
// Filters/GlobalExceptionFilter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using YourProject.Models;
using YourProject.Exceptions;
using System.Net;
namespace YourProject.Filters
{
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _env;
private readonly ILogger _logger;
public GlobalExceptionFilter(IWebHostEnvironment env, ILogger logger)
{
_env = env;
_logger = logger;
}
public void OnException(ExceptionContext context)
{
// 1. 记录日志(这是最重要的!生产环境查错全靠它)
_logger.LogError(context.Exception, "全局异常捕获:{Message}", context.Exception.Message);
var response = new ApiErrorResponse
{
Message = "服务器内部错误,请稍后再试。"
};
HttpStatusCode statusCode = HttpStatusCode.InternalServerError;
// 2. 处理自定义业务异常
if (context.Exception is BusinessException bizEx)
{
response.Message = bizEx.Message;
response.ErrorCode = bizEx.ErrorCode;
statusCode = HttpStatusCode.BadRequest; // 业务异常通常返回400
}
// 3. 处理模型验证异常(由 [ApiController] 特性抛出,实际是ModelState无效)
else if (context.Exception is BadHttpRequestException)
{
// 注意:模型验证错误通常由框架自动处理,但这里可作为兜底
response.Message = "请求参数无效";
statusCode = HttpStatusCode.BadRequest;
}
// 4. 处理其他未知异常
else
{
// 开发环境暴露详细信息
if (_env.IsDevelopment())
{
response.Detail = context.Exception.ToString(); // 包含堆栈跟踪
}
}
// 5. 构造返回结果
context.Result = new ObjectResult(response)
{
StatusCode = (int)statusCode
};
// 6. 标记异常已处理,阻止继续传播
context.ExceptionHandled = true;
}
}
}
踩坑提示1:日志记录一定要放在最前面,并且记录完整的异常对象(context.Exception),而不仅仅是消息。我曾经因为只记录了Message,在排查一个空引用异常时浪费了大量时间,因为不知道具体是哪一行代码出的问题。
四、注册与配置全局过滤器
创建好过滤器后,需要在Program.cs(或Startup.cs)中将其注册到服务容器和MVC管道中。
// Program.cs
using YourProject.Filters;
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddControllers(options =>
{
// 将全局异常过滤器作为服务添加
options.Filters.Add();
});
// 注册过滤器本身为Scoped或Singleton服务
builder.Services.AddScoped();
var app = builder.Build();
// ... 中间件配置
app.UseAuthorization();
app.MapControllers();
app.Run();
这样注册后,过滤器就会对所有Controller的Action生效。
五、处理模型验证错误(进阶)
ASP.NET Core自带模型验证,默认会返回一个ValidationProblemDetails对象。为了与我们自定义的ApiErrorResponse格式统一,我们可以重写这个行为。
// 在 Program.cs 的 AddControllers 配置中
builder.Services.AddControllers(options =>
{
options.Filters.Add();
})
.ConfigureApiBehaviorOptions(options =>
{
// 禁用默认的模型验证过滤器,由我们自己控制
options.SuppressModelStateInvalidFilter = true;
// 或者,自定义InvalidModelState响应工厂
options.InvalidModelStateResponseFactory = context =>
{
var errorResponse = new ApiErrorResponse
{
Message = "数据验证失败",
Errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
)
};
return new BadRequestObjectResult(errorResponse);
};
});
踩坑提示2:SuppressModelStateInvalidFilter设置为true时要小心,这意味着框架将不再自动响应400,你需要确保自己的异常过滤器或Action能妥善处理无效的模型状态。我建议优先使用InvalidModelStateResponseFactory进行自定义,这样更安全。
六、使用中间件作为最终防线
异常过滤器在MVC管道中工作,但如果异常发生在中间件中(比如在认证、静态文件处理时),过滤器可能捕获不到。这时,我们需要一个更外层的异常处理中间件作为最终防线。
// Middleware/ExceptionHandlingMiddleware.cs
using System.Net;
using System.Text.Json;
using YourProject.Models;
namespace YourProject.Middleware
{
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IWebHostEnvironment _env;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IWebHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "中间件捕获未处理异常");
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
var response = new ApiErrorResponse { Message = "处理请求时发生错误" };
// 可以根据exception类型设置不同的StatusCode和Message
context.Response.StatusCode = exception is BusinessException ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.InternalServerError;
if (_env.IsDevelopment())
{
response.Detail = exception.ToString();
}
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var jsonResponse = JsonSerializer.Serialize(response, jsonOptions);
await context.Response.WriteAsync(jsonResponse);
}
}
}
然后在Program.cs中非常早地使用这个中间件:
// Program.cs
app.UseMiddleware();
// 其他中间件,如 UseAuthentication, UseAuthorization 等
app.UseAuthorization();
app.MapControllers();
实战建议:异常过滤器(针对MVC Action)和异常处理中间件(针对整个管道)可以并存。通常,业务逻辑异常由过滤器处理更合适,因为它能访问MVC上下文;而中间件作为兜底,确保任何地方抛出的异常都不会导致原始堆栈信息泄露。在我的项目中,两者结合使用,形成了双重保险。
七、测试与总结
让我们写一个简单的Controller来测试一下:
// Controllers/TestController.cs
using Microsoft.AspNetCore.Mvc;
using YourProject.Exceptions;
namespace YourProject.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet("business-error")]
public IActionResult TriggerBusinessError()
{
throw new BusinessException("模拟业务异常:用户积分不足。", "INSUFFICIENT_CREDITS");
}
[HttpGet("unexpected-error")]
public IActionResult TriggerUnexpectedError()
{
var obj = default(string);
return Ok(obj.Length); // 这里会抛出 NullReferenceException
}
[HttpPost("validation-error")]
public IActionResult TriggerValidationError([FromBody] TestModel model)
{
if (!ModelState.IsValid)
{
// 由于我们配置了 InvalidModelStateResponseFactory,这里其实不需要手动返回错误。
// 但为了演示,我们可以直接返回,框架会自动处理。
return BadRequest(); // 框架会触发我们自定义的验证错误响应
}
return Ok("成功");
}
}
public class TestModel
{
[Required]
public string Name { get; set; }
[Range(1, 100)]
public int Age { get; set; }
}
}
启动项目,分别调用这三个接口,你会看到:
/api/test/business-error: 返回400状态码,JSON体包含我们自定义的错误消息和错误码。/api/test/unexpected-error: 返回500状态码,在生产环境只有友好提示,在开发环境会包含堆栈详情。/api/test/validation-error(POST一个无效数据): 返回400状态码,并且Errors字段会包含每个属性的验证错误信息。
至此,一个结构清晰、易于维护的全局异常处理机制就搭建完成了。它不仅能提升API的健壮性,还能为前后端协作提供一致的错误契约。记住,好的异常处理不仅是技术实现,更是对用户体验的重视。希望这篇教程能帮到你,如果你在实现过程中遇到其他问题,欢迎在源码库交流讨论。

评论(0)