写在前面
本文讲 API 工程化:RESTful 设计规范、Swagger 文档、统一错误处理、版本控制、序列化。这些是 API 从"能跑"到"专业"的关键。
一、RESTful API 设计
1.1 资源命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ✓ 用名词复数表示资源集合
GET /api/users 获取列表
POST /api/users 新建
GET /api/users/123 获取单个
PUT /api/users/123 全量更新
PATCH /api/users/123 部分更新
DELETE /api/users/123 删除
✗ 别用动词(RPC 风格)
/api/getUser /api/createUser /api/deleteUserById
✓ 嵌套表达从属关系
GET /api/users/123/orders 用户 123 的订单
GET /api/orders/456 全局订单(订单也有独立资源)
✓ 查询参数做过滤/排序/分页
GET /api/users?role=admin&sort=created&desc&page=2&size=20
|
1.2 HTTP 方法语义
1
2
3
4
5
6
7
8
9
| GET 安全、幂等、无副作用 (查询)
POST 非幂等 (新建)
PUT 幂等(全量替换) (更新)
PATCH 非幂等(部分更新) (更新)
DELETE 幂等 (删除)
幂等性:多次执行结果相同
GET/PUT/DELETE 幂等
POST/PATCH 非幂等
|
1.3 状态码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 2xx 成功
200 OK 请求成功(有 body)
201 Created 新建成功(带 Location 头)
204 No Content 成功无 body(DELETE 常用)
4xx 客户端错误
400 Bad Request 参数错误/验证失败
401 Unauthorized 未认证(没登录)
403 Forbidden 已认证但无权限
404 Not Found 资源不存在
409 Conflict 冲突(如重复创建)
422 Unprocessable 语义错误
429 Too Many 限流
5xx 服务端错误
500 Internal Error 服务器异常
502/503/504 网关/服务不可用/超时
|
1
2
3
| 401 vs 403 容易混:
401 — 你是谁?(没登录/Token 失效)
403 — 你没权限。(登录了,但权限不够)
|
二、Swagger / OpenAPI
2.1 基本接入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // 安装 Swashbuckle
// dotnet add package Swashbuckle.AspNetCore
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
// JWT 认证支持
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header. 例:Bearer {token}",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{ new OpenApiSecurityScheme { Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty<string>() }
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 访问 /swagger 看交互式文档
|
2.2 XML 注释文档
1
2
3
4
5
| <!-- .csproj 里启用 XML 文档 -->
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <!-- 忽略缺少注释的警告 -->
</PropertyGroup>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Program.cs
builder.Services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
/// <summary>获取用户信息</summary>
/// <param name="id">用户 ID</param>
/// <returns>用户详情</returns>
/// <response code="200">返回用户</response>
/// <response code="404">用户不存在</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(User), 200)]
[ProducesResponseType(404)]
public ActionResult<User> Get(int id) { ... }
|
三、统一错误处理
3.1 异常处理中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // 全局异常兜底(放管道最前面)
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try { await _next(context); }
catch (Exception ex)
{
_logger.LogError(ex, "未处理异常 {Path}", context.Request.Path);
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
code = 500,
message = app.Environment.IsDevelopment() ? ex.Message : "服务器内部错误"
}));
}
}
}
app.UseMiddleware<ExceptionMiddleware>(); // 注册(最前面)
|
3.2 ProblemDetails(标准错误格式)
ASP.NET Core 内置的标准化错误响应(RFC 7807):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // [ApiController] 自带的验证错误就是 ProblemDetails 格式
// { "type": "...", "title": "Validation error", "status": 400, ... }
// 自定义返回
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var user = _service.Get(id);
if (user == null)
return NotFound(new ProblemDetails
{
Title = "用户不存在",
Status = 404,
Detail = $"ID 为 {id} 的用户不存在"
});
return Ok(user);
}
|
3.3 自定义异常 + 映射
1
2
3
4
5
6
7
8
9
10
11
12
| // 业务异常
public class NotFoundException : Exception
{
public NotFoundException(string msg) : base(msg) { }
}
// 异常→状态码映射中间件
catch (NotFoundException ex)
{
context.Response.StatusCode = 404;
// 返回 ProblemDetails
}
|
3.4 统一响应包装(可选)
有些团队喜欢所有响应统一格式:
1
2
3
4
5
6
7
8
9
| public class ApiResponse<T>
{
public int Code { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}
// 但 RESTful 纯粹派认为:用 HTTP 状态码即可,不需要再包一层 code
// 两种风格都常见,团队统一即可
|
四、版本控制
API 演进时需要版本控制,避免破坏老客户端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 安装:Asp.Versioning.Mvc
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // 响应头显示支持版本
options.ApiVersionReader = new UrlSegmentApiVersionReader(); // URL 段
// 也可:Query string / Header
});
// 控制器标版本
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase { ... }
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase { ... }
// /api/v1/users → V1
// /api/v2/users → V2
|
五、序列化(System.Text.Json)
5.1 配置
1
2
3
4
5
6
7
8
9
| builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.WriteIndented = false;
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull; // null 不输出
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); // 枚举转字符串
});
|
5.2 常用特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class User
{
[JsonPropertyName("user_id")] // 自定义 JSON 字段名
public int Id { get; set; }
[JsonIgnore] // 不序列化
public string Password { get; set; }
[JsonPropertyOrder(1)] // 排序
public string Name { get; set; }
}
// 循环引用(EF Core 导航属性常见)
[JsonReferenceHandler(ReferenceHandler.IgnoreCycles)]
public class Order { ... }
|
5.3 驼峰与中文
1
2
3
4
5
6
7
| 默认 PascalCase(C# 风格)序列化后还是 PascalCase?
前端通常要 camelCase → 配 PropertyNamingPolicy = CamelCase
C# 的 UserId → JSON 的 userId
中文转义问题:
默认中文会被转义成 \uXXXX
加 Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) 保留中文
|
六、小结
- RESTful:名词资源、HTTP 方法语义、正确状态码(401 vs 403)
- Swagger:Swashbuckle 接入,XML 注释 + JWT 支持
- 错误处理:全局异常中间件 + ProblemDetails 标准格式
- 版本控制:URL 段 / Query / Header,ApiVersioning 库
- 序列化:System.Text.Json,camelCase、忽略 null、枚举字符串
下一篇讲进阶:过滤器、后台服务、健康检查、性能优化、部署。