C# WebAPI通用响应设计
·
C# WebAPI通用响应设计
一、设计概述
本设计用于统一 Web API 的返回结构,解决各控制器返回格式不一致、前端解析困难的问题。
- ApiResponse / ApiResponse<T>:统一的响应包装类(record),携带业务状态码、消息、数据、总数与时间戳。
- ApiControllerBase:API 控制器基类,提供
Success、Error等开箱即用的响应方法。业务控制器只需继承它,即可返回统一结构的 JSON。
设计约定:HTTP 状态码始终为 200,业务结果通过响应体中的
code判断。这样便于前端统一拦截处理。
二、文件结构
通用响应设计/
├── ApiResponse.cs // ResultType 枚举 + ApiResponse(非泛型) + ApiResponse<T>(泛型)
├── ApiControllerBase.cs // API 控制器基类(统一响应方法)
├── ExampleController.cs // 示例控制器(演示继承与调用)
2.1 ApiResponse
using System;
namespace PMS.Models.BaseCommDto
{
/// <summary>
/// 业务响应状态码
/// </summary>
public enum ResultTypeCode
{
/// <summary>成功</summary>
Success = 200,
/// <summary>请求参数错误</summary>
BadRequest = 400,
/// <summary>未授权(未登录或登录已过期)</summary>
Unauthorized = 401,
/// <summary>无权限(已登录但无权访问)</summary>
Forbidden = 403,
/// <summary>资源不存在</summary>
NotFound = 404,
/// <summary>参数校验失败</summary>
ValidationError = 422,
/// <summary>服务器内部错误</summary>
ServerError = 500,
/// <summary>业务失败(通用业务异常)</summary>
BusinessError = 600
}
/// <summary>
/// 通用通信响应包装器(非泛型版本,用于无数据负载场景)
/// </summary>
public record ApiResponse
{
/// <summary>
/// 业务状态码(200 表示成功,其他为具体错误码)
/// </summary>
public int Code { get; init; }
/// <summary>
/// 响应消息(成功提示或错误详情)
/// </summary>
public string Message { get; init; } = string.Empty;
/// <summary>
/// 数据负载
/// </summary>
public object? Data { get; init; }
/// <summary>
/// 总记录数(分页场景使用)
/// </summary>
public int? Count { get; init; }
/// <summary>
/// 时间戳(服务器响应时间)
/// </summary>
public DateTime Timestamp { get; init; } = DateTime.Now;
// ---------- 工厂方法 ----------
/// <summary>
/// 创建成功响应
/// </summary>
/// <param name="message">成功提示消息</param>
/// <param name="data">数据负载</param>
/// <param name="count">总记录数(分页场景)</param>
public static ApiResponse Success(string message = "操作成功!", object? data = null, int? count = null)
=> new() { Code = (int)ResultTypeCode.Success, Message = message, Data = data, Count = count };
/// <summary>
/// 创建失败响应
/// </summary>
/// <param name="message">错误提示消息</param>
/// <param name="code">业务状态码</param>
/// <param name="data">数据负载</param>
/// <param name="count">总记录数</param>
public static ApiResponse Error(string message = "操作失败!", int code = (int)ResultTypeCode.BusinessError, object? data = null, int? count = null)
=> new() { Code = code, Message = message, Data = data, Count = count };
/// <summary>创建请求参数错误响应</summary>
public static ApiResponse BadRequest(string message = "请求参数错误!")
=> Error(message, (int)ResultTypeCode.BadRequest);
/// <summary>创建未授权响应</summary>
public static ApiResponse Unauthorized(string message = "未授权,请先登录!")
=> Error(message, (int)ResultTypeCode.Unauthorized);
/// <summary>创建无权限访问响应</summary>
public static ApiResponse Forbidden(string message = "无权限访问!")
=> Error(message, (int)ResultTypeCode.Forbidden);
/// <summary>创建资源不存在响应</summary>
public static ApiResponse NotFound(string message = "请求的资源不存在!")
=> Error(message, (int)ResultTypeCode.NotFound);
}
/// <summary>
/// 通用通信响应包装器(泛型版本,用于携带强类型数据负载)
/// </summary>
/// <typeparam name="T">数据负载类型</typeparam>
public record ApiResponse<T>
{
/// <summary>
/// 业务状态码(200 表示成功,其他为具体错误码)
/// </summary>
public int Code { get; init; }
/// <summary>
/// 响应消息(成功提示或错误详情)
/// </summary>
public string Message { get; init; } = string.Empty;
/// <summary>
/// 数据负载
/// </summary>
public T? Data { get; init; }
/// <summary>
/// 总记录数(分页场景使用)
/// </summary>
public int? Count { get; init; }
/// <summary>
/// 时间戳(服务器响应时间)
/// </summary>
public DateTime Timestamp { get; init; } = DateTime.Now;
// ---------- 工厂方法 ----------
/// <summary>
/// 创建成功响应(带数据)
/// </summary>
/// <param name="data">数据负载</param>
/// <param name="message">成功提示消息</param>
/// <param name="count">总记录数(分页场景)</param>
public static ApiResponse<T> Success(T data, string message = "操作成功!", int? count = null)
=> new() { Code = (int)ResultTypeCode.Success, Message = message, Data = data, Count = count };
/// <summary>
/// 创建失败响应
/// </summary>
/// <param name="message">错误提示消息</param>
/// <param name="code">业务状态码</param>
/// <param name="data">数据负载</param>
/// <param name="count">总记录数</param>
public static ApiResponse<T> Error(string message = "操作失败!", int code = (int)ResultTypeCode.BusinessError, T? data = default, int? count = null)
=> new() { Code = code, Message = message, Data = data, Count = count };
/// <summary>创建请求参数错误响应</summary>
public static ApiResponse<T> BadRequest(string message = "请求参数错误!")
=> Error(message, (int)ResultTypeCode.BadRequest);
/// <summary>创建未授权响应</summary>
public static ApiResponse<T> Unauthorized(string message = "未授权,请先登录!")
=> Error(message, (int)ResultTypeCode.Unauthorized);
/// <summary>创建无权限访问响应</summary>
public static ApiResponse<T> Forbidden(string message = "无权限访问!")
=> Error(message, (int)ResultTypeCode.Forbidden);
/// <summary>创建资源不存在响应</summary>
public static ApiResponse<T> NotFound(string message = "请求的资源不存在!")
=> Error(message, (int)ResultTypeCode.NotFound);
}
}
2.2 ApiControllerBase.cs 控制器基类
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using PMS.Models.BaseCommDto;
namespace PMS.Web.Controllers.Base
{
/// <summary>
/// API 控制器基类
/// 所有业务控制器继承此类,统一使用 ApiResponse 响应格式,
/// 调用 Success / Error 等方法即可返回统一结构的 JSON 数据。
/// </summary>
[ApiController]
public class ApiControllerBase : ControllerBase
{
#region 成功响应
/// <summary>
/// 返回成功响应(无数据负载)
/// </summary>
/// <param name="message">成功提示消息</param>
/// <returns>统一格式的成功 JSON</returns>
protected IActionResult Success(string message = "操作成功!")
=> Ok(ApiResponse.Success(message));
/// <summary>
/// 返回成功响应(带数据负载)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="data">数据负载</param>
/// <param name="message">成功提示消息</param>
/// <param name="count">总记录数(分页场景)</param>
/// <returns>统一格式的成功 JSON</returns>
protected IActionResult Success<T>(T data, string message = "操作成功!", int? count = null)
=> Ok(ApiResponse<T>.Success(data, message, count));
/// <summary>
/// 返回成功响应(分页列表场景)
/// </summary>
/// <typeparam name="T">列表元素类型</typeparam>
/// <param name="items">当前页数据集合</param>
/// <param name="total">总记录数</param>
/// <param name="message">成功提示消息</param>
/// <returns>统一格式的成功 JSON(data 为列表,count 为总数)</returns>
protected IActionResult SuccessPage<T>(IEnumerable<T> items, int total, string message = "操作成功!")
=> Ok(ApiResponse<IEnumerable<T>>.Success(items, message, total));
#endregion
#region 失败响应
/// <summary>
/// 返回失败响应(无数据负载)
/// </summary>
/// <param name="message">错误提示消息</param>
/// <param name="code">业务状态码,默认 BusinessError(600)</param>
/// <returns>统一格式的错误 JSON</returns>
protected IActionResult Error(string message = "操作失败!", ResultTypeCode code = ResultTypeCode.BusinessError)
=> Ok(ApiResponse.Error(message, (int)code));
/// <summary>
/// 返回失败响应(带数据负载)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="message">错误提示消息</param>
/// <param name="data">数据负载</param>
/// <param name="code">业务状态码,默认 BusinessError(600)</param>
/// <returns>统一格式的错误 JSON</returns>
protected IActionResult Error<T>(string message, T? data = default, ResultTypeCode code = ResultTypeCode.BusinessError)
=> Ok(ApiResponse<T>.Error(message, (int)code, data));
#endregion
#region 常见错误场景
/// <summary>返回请求参数错误响应</summary>
protected IActionResult BadRequestResult(string message = "请求参数错误!")
=> Ok(ApiResponse.BadRequest(message));
/// <summary>返回未授权响应(未登录或登录已过期)</summary>
protected IActionResult UnauthorizedResult(string message = "未授权,请先登录!")
=> Ok(ApiResponse.Unauthorized(message));
/// <summary>返回无权限访问响应(已登录但无权访问)</summary>
protected IActionResult ForbiddenResult(string message = "无权限访问!")
=> Ok(ApiResponse.Forbidden(message));
/// <summary>返回资源不存在响应</summary>
protected IActionResult NotFoundResult(string message = "请求的资源不存在!")
=> Ok(ApiResponse.NotFound(message));
/// <summary>返回服务器内部错误响应</summary>
protected IActionResult ServerErrorResult(string message = "服务器内部错误!")
=> Ok(ApiResponse.Error(message, (int)ResultTypeCode.ServerError));
#endregion
}
}
三、响应格式定义
3.1 ApiResponse(非泛型,无强类型数据)
| 字段 | 类型 | 说明 |
|---|---|---|
| Code | int | 业务状态码,200 表示成功 |
| Message | string | 提示消息 |
| Data | object? | 数据负载 |
| Count | int? | 总记录数(分页) |
| Timestamp | DateTime | 服务器响应时间 |
3.2 ApiResponse<T>(泛型,携带强类型数据)
字段同上,Data 类型为 T?,序列化时保持强类型结构。
3.3 JSON 输出示例
成功·带数据:
{
"code": 200,
"message": "查询成功",
"data": { "id": 1, "name": "张三", "email": "zhangsan@example.com" },
"count": null,
"timestamp": "2026-08-20T10:30:00+08:00"
}
成功·分页列表:
{
"code": 200,
"message": "查询成功",
"data": [
{ "id": 1, "name": "张三" },
{ "id": 2, "name": "李四" }
],
"count": 3,
"timestamp": "2026-08-20T10:30:00+08:00"
}
失败·无数据:
{
"code": 404,
"message": "未找到 ID 为 5 的用户",
"data": null,
"count": null,
"timestamp": "2026-08-20T10:30:00+08:00"
}
四、业务状态码(ResultType)
| 枚举值 | 数值 | 含义 |
|---|---|---|
| Success | 200 | 成功 |
| BadRequest | 400 | 请求参数错误 |
| Unauthorized | 401 | 未授权(未登录或登录已过期) |
| Forbidden | 403 | 无权限(已登录但无权访问) |
| NotFound | 404 | 资源不存在 |
| ValidationError | 422 | 参数校验失败 |
| ServerError | 500 | 服务器内部错误 |
| BusinessError | 600 | 业务失败(通用) |
五、ApiControllerBase 基类方法
| 方法 | 参数 | 说明 | 返回 Code |
|---|---|---|---|
Success(message) | message 默认"操作成功!" | 成功·无数据 | 200 |
Success<T>(data, message, count?) | data 数据;message 提示;count 总数 | 成功·带数据 | 200 |
SuccessPage<T>(items, total, message) | items 当前页集合;total 总数 | 成功·分页列表 | 200 |
Error(message, code) | message 默认"操作失败!";code 默认 BusinessError | 失败·无数据 | 默认 600 |
Error<T>(message, data, code) | data 数据负载 | 失败·带数据 | 默认 600 |
BadRequestResult(message) | 请求参数错误 | 参数错误 | 400 |
UnauthorizedResult(message) | 未授权 | 未登录/过期 | 401 |
ForbiddenResult(message) | 无权限 | 禁止访问 | 403 |
NotFoundResult(message) | 资源不存在 | 未找到 | 404 |
ServerErrorResult(message) | 服务器内部错误 | 服务异常 | 500 |
说明:
Error("提示", ResultType.NotFound)也可用枚举指定任意业务状态码,方法内部自动转为 int。
六、快速上手
6.1 注册服务(Program.cs)
builder.Services.AddControllers();
[ApiController] 与路由特性已标注在基类上,子类自动继承。
6.2 控制器继承基类
using Microsoft.AspNetCore.Mvc;
using PMS.Web.Controllers.Base;
[Route("api/[controller]")]
public class UserController : ApiControllerBase
{
// 无数据成功
[HttpPost("logout")]
public IActionResult Logout()
=> Success("退出登录成功");
// 带数据成功
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
var user = _service.GetById(id);
if (user is null)
return NotFoundResult($"未找到 ID 为 {id} 的用户");
return Success(user, "查询成功");
}
// 分页成功
[HttpGet("list")]
public IActionResult List(int pageIndex = 1, int pageSize = 10)
{
var (items, total) = _service.GetPage(pageIndex, pageSize);
return SuccessPage(items, total, "查询成功");
}
// 失败响应
[HttpPost]
public IActionResult Create(UserDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Name))
return Error("用户名不能为空", code: ResultType.ValidationError);
// ...
return Success(dto, "新增成功");
}
}
6.3 异步控制器
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var user = await _service.GetByIdAsync(id);
if (user is null)
return NotFoundResult($"未找到 ID 为 {id} 的用户");
await _service.DeleteAsync(id);
return Success($"用户 {user.Name} 已删除");
}
6.4 不使用基类时手动构造
return Ok(ApiResponse.Success("操作成功"));
return Ok(ApiResponse<User>.Success(user, "查询成功"));
return Ok(ApiResponse.Error("操作失败", (int)ResultType.NotFound));
七、注意事项
- HTTP 状态码固定为 200:统一走
Ok(...)。若某接口需要真正的 HTTP 非 200 状态码(如 401 供网关判断),可改用StatusCode(401, ApiResponse.Unauthorized())。 - JSON 命名策略:默认采用
System.Text.Json的 camelCase(小驼峰)输出;如项目配置了其他命名策略,序列化结果随之变化。 Success("文本")与Success<T>(data)重载:传字符串时命中"无数据"重载;传对象时命中"带数据"重载(泛型自动推断),二者不会歧义。- 与前端约定:前端应统一判断
code === 200视为成功,其余均为失败,并读取message提示。
八、扩展建议
- 全局异常处理:可在中间件 /
IExceptionFilter中捕获未处理异常,统一返回ServerErrorResult格式,避免控制器内重复 try-catch。 - 新增业务码:业务需要时在
ResultType枚举中追加,如NoStock = 601,并直接用于Error(message, ResultType.NoStock)。 - 模型校验:配合
[ApiController]自动 400 响应,可通过ModelState提取第一条错误信息,包装成ValidationError返回。
九、使用示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PMS.Models.BaseCommDto;
using PMS.Web.Controllers.Base;
namespace PMS.Web.Controllers
{
/// <summary>
/// 示例控制器
/// 演示如何继承 ApiControllerBase 并使用 Success / Error 等方法返回统一格式的响应。
/// </summary>
[Route("api/[controller]")]
public class ExampleController : ApiControllerBase
{
private static readonly List<UserDto> Users = new()
{
new UserDto { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
new UserDto { Id = 2, Name = "李四", Email = "lisi@example.com" },
new UserDto { Id = 3, Name = "王五", Email = "wangwu@example.com" }
};
/// <summary>
/// 分页查询用户列表
/// GET /api/Example/list?pageIndex=1&pageSize=2
/// </summary>
[HttpGet("list")]
public IActionResult GetList([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10)
{
var items = Users.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return SuccessPage(items, Users.Count, "查询成功");
}
/// <summary>
/// 根据 ID 查询用户
/// GET /api/Example/1
/// </summary>
[HttpGet("{id:int}")]
public IActionResult GetById(int id)
{
if (id <= 0)
return BadRequestResult("无效的用户 ID");
var user = Users.FirstOrDefault(u => u.Id == id);
if (user is null)
return NotFoundResult($"未找到 ID 为 {id} 的用户");
return Success(user, "查询成功");
}
/// <summary>
/// 新增用户
/// POST /api/Example
/// </summary>
[HttpPost]
public IActionResult Create([FromBody] UserDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Name))
return Error("用户名不能为空", code: ResultType.ValidationError);
dto.Id = Users.Count == 0 ? 1 : Users.Max(u => u.Id) + 1;
Users.Add(dto);
return Success(dto, "新增成功");
}
/// <summary>
/// 删除用户(异步示例)
/// DELETE /api/Example/1
/// </summary>
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var user = Users.FirstOrDefault(u => u.Id == id);
if (user is null)
return NotFoundResult($"未找到 ID 为 {id} 的用户");
// 模拟异步业务操作
await Task.Delay(10);
Users.Remove(user);
return Success($"用户 {user.Name} 已删除");
}
}
/// <summary>示例数据模型</summary>
public record UserDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Email { get; set; }
}
}
更多推荐
所有评论(0)