ASP.NET Core 学习笔记(四):API 设计与错误处理

写在前面

本文讲 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、枚举字符串

下一篇讲进阶:过滤器、后台服务、健康检查、性能优化、部署。