1. RESTful API 设计原则
一句话总结: REST 以资源为中心,用 HTTP 方法表达操作语义、用状态码表达结果,良好的资源命名与超媒体链接是 API 可演进性的基础。
RESTful 设计把一切抽象为资源(Resource),每个资源有唯一的 URL,操作通过 HTTP 方法表达:GET 读、POST 建、PUT 全量更新、PATCH 局部更新、DELETE 删。URL 只含名词复数,动词交给方法。
// 反例:把动词放进 URL
GET /api/getOrders
POST /api/createOrder
GET /api/deleteOrder/5
// 正例:资源 + 方法
GET /api/orders // 列表
POST /api/orders // 新建
GET /api/orders/{id} // 单个
PUT /api/orders/{id} // 全量更新
PATCH /api/orders/{id} // 局部更新
DELETE /api/orders/{id} // 删除
| 原则 | 要点 |
|---|---|
| 资源命名 | 复数名词、小写、连字符分隔 |
| 方法语义 | GET 幂等只读、PUT 幂等、POST 非幂等 |
| 状态码 | 200/201/204/400/404/409 各司其职 |
| 无状态 | 服务端不保存客户端会话状态 |
| 可发现性 | 集合端点返回子资源链接 |
避坑: 不要把查询参数复杂化到「伪 RPC」。筛选、排序、分页用查询字符串(
?status=paid&page=2),但仍然保持资源语义,而不是?action=computeDiscount。状态码宁可用 409 表达业务冲突,也不要一律返回 200 + 错误码字段。
2. Minimal API 与传统 Controller
一句话总结: Minimal API 用最少的样板暴露 HTTP 端点,适合小型服务与演示项目;Controller 模式适合团队大型应用,两者在同一应用中可以共存。
Minimal API 把「路由 + 处理逻辑」压缩成一行 Lambda,没有 Controller 类、没有 [ApiController]、没有基类继承,依赖注入与配置仍完整保留。它非常适合微服务、健康检查、BFF 等场景。
// Program.cs —— Minimal API 最小示例
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.MapGet("/orders/{id}", async (IOrderService svc, int id) =>
{
var order = await svc.GetByIdAsync(id);
return order is null ? Results.NotFound() : Results.Ok(order);
});
app.MapPost("/orders", async (IOrderService svc, CreateOrderRequest req) =>
{
var id = await svc.CreateAsync(req);
return Results.Created($"/orders/{id}", id);
});
app.Run();
// 传统 Controller 等价实现
[ApiController]
[Route("api/orders")]
public class OrdersController(IOrderService svc) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var order = await svc.GetByIdAsync(id);
return order is null ? NotFound() : Ok(order);
}
[HttpPost]
public async Task<IActionResult> Create(CreateOrderRequest req)
{
var id = await svc.CreateAsync(req);
return Created($"/api/orders/{id}", id);
}
}
| 维度 | Minimal API | Controller |
|---|---|---|
| 样板量 | 极低 | 较高 |
| 路由过滤 | MapXxx + 通配符 | 属性路由 + 约束 |
| 自动模型验证 | 需手动调用 | [ApiController] 自动 |
| 适合场景 | 小服务、演示、工具端点 | 大型领域 API |
一句话: 选择标准不是「哪个更酷」,而是团队的认知负载。几十个端点的小服务用 Minimal API 清爽直接;几十个实体的领域 API 用 Controller 更便于组织验证、过滤器与约定。
3. Minimal API 路由与参数绑定
一句话总结: Minimal API 的路由支持约束与通配符,参数通过委托签名自动绑定,lambda 参数的类型决定了来源是路径、查询还是请求体。
Minimal API 的参数绑定规则:{id:int} 路径段绑定到同名的简单类型参数;复杂类型默认从 JSON 请求体反序列化;[FromQuery]、[FromHeader] 等特性可显式指定来源。路由约束让非法输入提前 404。
// 路由约束:int 只匹配整数,不符合返回 404
app.MapGet("/orders/{id:int}", (int id) => ...);
app.MapGet("/orders/{code:length(4,16)}", (string code) => ...);
// 查询与 Header 显式来源
app.MapGet("/orders", (
[FromQuery] int page = 1,
[FromQuery] int size = 20,
[FromHeader(Name = "X-Request-Id")] string? requestId) =>
{
return Results.Ok(new { page, size, requestId });
});
// 请求体复杂类型
app.MapPost("/orders", (CreateOrderRequest req) => ...);
// 文件上传
app.MapPost("/upload", async (IFormFile file) =>
{
await using var fs = File.Create(Path.Combine("uploads", file.FileName));
await file.CopyToAsync(fs);
return Results.Ok(new { name = file.FileName, size = file.Length });
});
| 绑定来源 | 写法 |
|---|---|
| 路径参数 | {id:int} → int id |
| 查询参数 | 同名简单类型参数 |
| 请求体 | 复杂类型自动 JSON 反序列化 |
| Header | [FromHeader] 特性 |
| 文件 | IFormFile 类型 |
避坑: 简单类型参数默认不是从请求体取,而是从路径/查询取。想让 int 从 body 来必须用
[FromBody]。另外 Minimal API 不自动做模型验证(Controller 的[ApiController]会),参数校验要自己写,或者显式调用Results.ValidationProblem。
4. OpenAPI 与 Swagger 文档生成
一句话总结: 通过 AddOpenApi 自动生成 OpenAPI 文档,Swagger UI 提供交互式调试界面,按需用特性或 XML 注释增强文档信息。
ASP.NET Core 9+ 内置 Microsoft.AspNetCore.OpenApi,一行代码即可生成 OpenAPI 3 文档;Swagger UI 让前端与测试可以直接在浏览器里试请求。Minimal API 的端点也能通过 WithName、WithSummary、Produces 等扩展方法补充元数据。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); // 生成 /openapi/v1.json
var app = builder.Build();
app.MapOpenApi(); // 暴露 JSON 端点
app.MapSwagger(); // 可选:Swagger UI 页面
// 为端点补充 OpenAPI 元数据
app.MapGet("/orders/{id:int}", async (IOrderService svc, int id) =>
await svc.GetByIdAsync(id) is { } o ? Results.Ok(o) : Results.NotFound())
.WithName("GetOrder")
.WithSummary("按 ID 获取订单")
.WithDescription("返回订单详情,不存在时返回 404")
.Produces<Order>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
app.Run();
// 生成的 openapi/v1.json 片段
{
"openapi": "3.0.1",
"paths": {
"/orders/{id}": {
"get": {
"operationId": "GetOrder",
"summary": "按 ID 获取订单",
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
],
"responses": {
"200": { "description": "Success" },
"404": { "description": "Not Found" }
}
}
}
}
}
| 工具 | 作用 |
|---|---|
AddOpenApi() | 注册 OpenAPI 文档生成 |
MapOpenApi() | 暴露 /openapi/v1.json |
MapSwagger() | Swagger UI 交互页 |
WithSummary/WithName | 端点文档元数据 |
Produces<T> | 声明响应类型与状态码 |
避坑: 生产环境不要把 Swagger UI 暴露到公网——它是攻击面的信息源。正确做法是仅开发/测试环境启用,生产关闭或加鉴权。文档生成依赖返回类型推断,lambda 里显式
Results.Ok<T>(...)能让类型更准确。
5. 模型绑定与 Data Annotation 验证
一句话总结: 模型绑定把请求数据映射到参数对象,Data Annotation 提供声明式校验规则,Controller 模式自动触发而 Minimal API 需要手动调用 Validate。
模型绑定从请求的路径、查询、Header、Body 组装出目标对象。校验则靠 [Required]、[Range]、[StringLength]、[EmailAddress] 等特性描述规则。Controller 的 [ApiController] 会在绑定后自动验证并返回 400。
// 请求模型:声明式校验规则
public class CreateOrderRequest
{
[Required]
[StringLength(64, MinimumLength = 2)]
public string CustomerName { get; set; } = string.Empty;
[Range(0.01, 1_000_000)]
public decimal Total { get; set; }
[Required]
public List<OrderLineDto> Lines { get; set; } = [];
}
public class OrderLineDto
{
[Required]
public int ProductId { get; set; }
[Range(1, 100)]
public int Quantity { get; set; }
}
// Minimal API:手动校验(Controller 自动)
app.MapPost("/orders", async (IOrderService svc, CreateOrderRequest req) =>
{
// 需要引入 Microsoft.AspNetCore.Http.HttpResults 扩展的 Validate 方法
// 或自行校验后返回 ValidationProblem
if (!Validator.TryValidateObject(req, new ValidationContext(req), out var errors))
{
return Results.ValidationProblem(
errors.GroupBy(e => e.MemberNames.FirstOrDefault() ?? "")
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage ?? "").ToArray()));
}
var id = await svc.CreateAsync(req);
return Results.Created($"/orders/{id}", id);
});
| 校验手段 | 说明 |
|---|---|
[Required] / [Range] | 内置 Data Annotation |
[StringLength] | 长度约束 |
IValidatableObject | 跨字段自定义规则 |
FluentValidation | 链式规则,更强大 |
Validator.TryValidateObject | 手动触发校验 |
避坑: Controller 的自动验证返回 400 前不会进入方法体,而 Minimal API 全靠自觉——漏了
Validate就漏了校验,脏数据直达业务层。复杂跨字段规则(如「折扣价不能高于原价」)建议用IValidatableObject或 FluentValidation。
6. API 版本控制
一句话总结: API 演进必须考虑兼容性,URL 路径版本是最直观的方案,配合响应与文档的版本隔离,让新旧客户端并行使用。
版本控制解决「老客户端还在用、新接口又要上」的兼容问题。ASP.NET Core 通过 AddApiVersioning 支持 URL 路径、查询串、Header 三种策略。路径版本(/api/v2/orders)最直观,也是兼容成本最低的默认选择。
// 引入 Asp.Versioning.Http 包
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // 响应头返回 api-supported-versions
}).AddApiExplorer(); // 让 OpenAPI 显示各版本
var app = builder.Build();
var v1 = app.MapGroup("/api/v1/orders");
v1.MapGet("/", async (IOrderService svc) => Results.Ok(await svc.ListV1Async()));
v1.MapGet("/{id:int}", async (IOrderService svc, int id) => ...);
var v2 = app.MapGroup("/api/v2/orders");
v2.MapGet("/", async (IOrderService svc) => Results.Ok(await svc.ListV2Async()));
| 版本策略 | 载体 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径 | /api/v2/orders | 直观、缓存友好 | URL 长期暴露旧版 |
| 查询串 | ?api-version=2 | URL 干净 | 容易被遗忘 |
| Header | X-Version: 2 | 路径不变 | 不直观 |
| Media Type | Accept: vnd.api.v2+json | RESTful 纯正 | 实现复杂 |
避坑: 版本不是「加个 v2 就完事」。每个版本都要有独立的生命周期与下线计划,废弃版本应提前声明并返回
Deprecation头。改动兼容(加字段、加可选参数)完全不需要新版本,只有破坏性变更才升版本。
7. 错误处理与统一返回
一句话总结: 统一的问题详情(RFC 7807)让错误可机器消费,全局异常中间件把未处理异常收敛成结构化响应,避免堆栈泄漏。
ASP.NET Core 内置 ProblemDetails 标准(RFC 7807),Results.Problem 与 IExceptionHandler 让错误响应结构一致。异常处理中间件兜底所有未捕获异常,开发环境返回详情、生产环境隐藏内部错误。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(); // 注册 ProblemDetails 服务
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
var app = builder.Build();
app.UseExceptionHandler(); // 启用全局异常中间件
app.MapGet("/orders/{id:int}", async (IOrderService svc, int id) =>
{
var order = await svc.GetByIdAsync(id);
return order is null
? Results.Problem(
statusCode: 404,
title: "订单不存在",
detail: $"找不到 ID 为 {id} 的订单")
: Results.Ok(order);
});
// 全局异常处理:收敛未捕获异常
public class GlobalExceptionHandler : IExceptionHandler
{
public ValueTask<bool> TryHandleAsync(
HttpContext ctx, Exception ex, CancellationToken ct)
{
ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
ctx.Response.ContentType = "application/problem+json";
return ctx.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = 500,
Title = "服务内部错误",
Detail = ctx.RequestServices.GetService<IHostEnvironment>()?
.IsDevelopment() == true ? ex.ToString() : null
}, ct).ContinueWith(_ => new ValueTask<bool>(true)).GetAwaiter().GetResult() is var r
? r : new ValueTask<bool>(true);
}
}
| 场景 | 响应 |
|---|---|
| 参数校验失败 | 400 ValidationProblemDetails |
| 资源不存在 | 404 ProblemDetails |
| 业务冲突 | 409 ProblemDetails |
| 未处理异常 | 500 ProblemDetails(隐藏堆栈) |
避坑: 别把
Exception的堆栈直接写进生产响应——那是信息泄漏。生产环境只给稳定 ID(如traceId),把完整堆栈交给日志系统。ProblemDetails 的type字段应指向一个可读的文档 URL,而不是空字符串。
8. 总结
| 环节 | 要点 |
|---|---|
| REST 设计 | 资源 + 方法 + 状态码,动词不进 URL |
| Minimal vs Controller | 小服务 Minimal,大领域 Controller,可共存 |
| 路由绑定 | 路径约束 + 委托参数自动绑定 |
| OpenAPI | AddOpenApi 生成文档,Swagger UI 调试 |
| 验证 | Data Annotation 声明规则,Minimal 需手动触发 |
| 版本控制 | URL 路径版本最直观,兼容变更不升版本 |
| 错误处理 | ProblemDetails 统一结构 + 全局异常兜底 |
Web API 的价值在于用 HTTP 语义表达业务,让客户端与工具都能理解。REST 的克制(资源化、方法化、状态码化)与 Minimal API 的简洁并不冲突——前者是设计原则,后者是实现手段。把契约(OpenAPI)、验证(Data Annotation)、错误(ProblemDetails)这三样固定下来,API 的可演进性与可协作性就有了地基。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。