.NET Core 3.1 WebApi向Vue应用返回用户错误的优雅方案咨询
落地方案:基于.NET Core 3.1 WebApi的统一错误返回机制
核心思路调整
彻底放弃“所有业务返回类继承BaseDataObject”的设计——这套设计的本质问题是把错误元数据和业务模型强绑定,导致接口返回的JSON结构随业务类型(单对象/列表)变化,前端需要写额外分支判断。
替换为独立于业务模型的全局统一响应外层结构,保证不管接口返回的是单条对象、列表、基本类型,外层JSON格式完全固定,前端不需要做任何结构判断。
统一响应类定义:
/// <summary> /// 全局固定API响应外层结构 /// </summary> public class ApiResponse { /// <summary> /// 错误严重级别/类型,无错误时为null /// </summary> public string ErrorType { get; set; } /// <summary> /// 面向用户展示的错误信息,无错误时为null /// </summary> public string ErrorMsg { get; set; } /// <summary> /// 实际业务数据,单对象/列表/空值都存在该字段下 /// </summary> public object Data { get; set; } }
这套结构直接解决第一个痛点:
- 不管接口返回的是单条业务对象、还是业务对象列表,前端始终可以固定流程:先检查
errorMsg字段是否存在,存在则弹窗展示错误;不存在则直接从data字段读取实际业务内容,不需要判断data是单对象还是数组 - 不需要让任何业务类继承错误基类,没有冗余字段,序列化后的JSON体积更轻
全局统一异常处理:删除所有接口内的重复catch逻辑
基于.NET Core的中间件机制实现全局异常捕获,彻底替代每个Action里手写try-catch的重复代码,覆盖所有业务错误场景:
第一步:定义业务异常类
和原有体系保持一致:
public class ApiErrorException : Exception { /// <summary> /// 自定义错误严重级别 /// </summary> public string Type { get; } public ApiErrorException(string message, string type = "warning") : base(message) { Type = type; } }
第二步:编写全局异常处理中间件
统一捕获所有抛出的异常,按照统一结构返回响应:
public class GlobalExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<GlobalExceptionMiddleware> _logger; public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (ApiErrorException ex) { // 预期内的业务校验错误,打警告日志,不需要记录全堆栈 _logger.LogWarning(ex, "业务校验触发错误"); await WriteResponse(context, ex.Message, ex.Type, null); } catch (Exception ex) { // 未捕获的系统异常,打错误日志带堆栈,返回通用友好提示 _logger.LogError(ex, "系统未处理异常"); await WriteResponse(context, "系统繁忙,请稍后重试", "error", null); } } private async Task WriteResponse(HttpContext context, string errorMsg, string errorType, object data) { // 兼容原MVC体系的习惯:业务错误也返回200状态码,前端通过响应字段判断错误 context.Response.StatusCode = StatusCodes.Status200OK; context.Response.ContentType = "application/json"; var response = new ApiResponse { ErrorMsg = errorMsg, ErrorType = errorType, Data = data }; await context.Response.WriteAsJsonAsync(response); } }
第三步:注册中间件与定制模型验证响应
在Startup.cs中配置管道顺序(全局异常中间件必须放在所有路由、MVC中间件之前):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 其他前置中间件(如静态文件、路由匹配) app.UseMiddleware<GlobalExceptionMiddleware>(); // 后续中间件(授权、MVC端点映射) app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => endpoints.MapControllers()); }
针对.NET Core WebApi默认的模型验证响应(返回400状态码和ProblemDetails结构),定制为和全局异常完全一致的返回格式,覆盖“适配验证错误”场景:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .ConfigureApiBehaviorOptions(options => { // 模型验证失败时,只返回第一条错误信息,符合“每次单条错误”的原有规则 options.InvalidModelStateResponseFactory = context => { var firstErrorMessage = context.ModelState.Values .SelectMany(v => v.Errors) .FirstOrDefault()?.ErrorMessage ?? "参数校验失败"; return new OkObjectResult(new ApiResponse { ErrorMsg = firstErrorMessage, ErrorType = "warning", Data = null }); }; }); }
完成配置后,所有场景的错误返回逻辑完全统一:
- 手动抛出的
ApiErrorException - 模型参数校验失败
- 未捕获的系统异常
都会返回结构完全一致的JSON,不需要在任何接口内写try-catch逻辑。
简化正常业务返回的辅助方法
不需要写复杂的泛型约束来适配BaseDataObject派生类,只需要简单的静态辅助方法统一包装正常返回的业务数据:
public static class ApiResponseHelper { /// <summary> /// 成功返回任意类型的业务数据(单对象/列表/空值均可) /// </summary> public static ApiResponse Success<T>(T data) { return new ApiResponse { ErrorMsg = null, ErrorType = null, Data = data }; } }
Controller层的Action写法示例,不管返回单条还是列表,结构完全一致:
// 返回单条业务对象 [HttpGet("single")] public async Task<ApiResponse> GetSingleAdData(int id) { var data = await _studentService.GetAdDataById(id); return ApiResponseHelper.Success(data); } // 返回业务对象列表 [HttpGet("list")] public async Task<ApiResponse> GetAllAdData() { var data = await _studentService.GetAllAdDataList(); return ApiResponseHelper.Success(data); }
方案对比
- 对比原有让所有业务类继承BaseDataObject的方案:不需要修改任何现有业务类的继承关系,没有冗余的序列化字段,单条/列表返回格式完全统一,前端不需要做分支判断
- 对比每个接口手写catch的方案:全局异常处理接管所有错误逻辑,没有重复代码,不会出现漏写try-catch导致的错误响应格式不统一的问题
- 对比之前尝试的泛型约束方案:不需要对业务类做任何继承限制,泛型辅助方法没有额外类型约束,适配所有场景
内容的提问来源于stack exchange,提问作者Eric Brown - Cal
相关产品推荐
相关产品推荐

