直接用 ActionResult<T> 不够用,因其不提供统一的 code、message、data 响应结构,无法满足前端对业务状态码和标准化格式的需求。

为什么直接用 ActionResult<T> 不够用
因为前端需要稳定结构:每次响应都得带 code、message、data,而原生 ActionResult<T> 或 IActionResult 返回的是裸数据或 HTTP 状态码,不带业务状态码(比如 20001 表示参数错误),也不统一包裹 message。硬塞到控制器里手动 new 包装对象,重复代码多、易漏、难维护。
定义泛型响应类要避开三个坑
常见错误是把 code 设成 int 却没约定含义,或者让 data 为 object 导致序列化丢失泛型信息,又或者忘了处理 null 数据时的默认值。
-
code建议用int,但必须配套静态类(如ApiResultCode)定义常量,避免魔数散落 -
data必须声明为TData泛型字段,不是object;否则反序列化时前端拿不到真实类型推导 - 构造函数里对
data = default要显式允许 null(尤其TData是值类型时),可加where TData : class或用可空泛型约束
示例精简版:
public class ApiResult<TData>
{
public int Code { get; set; }
public string Message { get; set; } = string.Empty;
public TData? Data { get; set; }
public static ApiResult<TData> Success(TData? data = default, string message = "OK") =>
new() { Code = 200, Message = message, Data = data };
public static ApiResult<TData> Fail(int code, string message) =>
new() { Code = code, Message = message };
}
全局统一包装需绕过 ObjectResult 的自动转换
ASP.NET Core 默认把返回值直接序列化为 JSON,不会走你写的 ApiResult<T> 构造逻辑。想让所有 return Ok(xxx) 都变成 ApiResult.Success(xxx),不能靠中间件改响应体(太晚、已序列化),得在模型绑定后、序列化前拦截。
- 最稳的方式是自定义
ActionFilter,重写OnResultExecutionAsync - 只包装
ObjectResult和OkObjectResult,跳过EmptyResult、StatusCodeResult等原生状态响应 - 注意判断
result.Value是否已是ApiResult<*>类型,避免套娃包装
关键逻辑节选:
if (result.Result is ObjectResult objectResult &&
objectResult.Value != null &&
objectResult.Value.GetType() != typeof(ApiResult<>))
{
var genericType = typeof(ApiResult<>).MakeGenericType(objectResult.Value.GetType());
var successMethod = typeof(ApiResult<>).GetMethod("Success").MakeGenericMethod(objectResult.Value.GetType());
var wrapped = successMethod.Invoke(null, new[] { objectResult.Value, "OK" });
context.Result = new OkObjectResult(wrapped);
}
异常也要进统一格式,但别吞掉 StatusCode
用 UseExceptionHandler 中间件捕获异常时,如果直接返回 ApiResult.Fail(500, ex.Message),HTTP 状态码仍是 200 —— 因为中间件默认用 OkObjectResult。前端靠状态码做网络层判断,不能丢。
- 手动 new
ObjectResult(new ApiResult<object>(...))并设置StatusCode字段 - 或更干净地:抛出自定义异常(如
BusinessException),在 filter 里统一 catch 并映射到对应code和StatusCode - 特别注意
ValidationException:ModelBinding 失败时走的是BadRequestObjectResult,要单独适配,不然校验失败也返回 200 + 错误 code
真正容易被忽略的是:前端可能同时依赖 HTTP 状态码(如 401 跳登录)和业务 code(如 40001 表示 token 过期),两者得共存,不能只靠一个。


















