4. 服务契约与API设计
在微服务架构中,服务之间的调用不再依赖进程内的接口,而是通过网络进行远程通信。这意味着,服务提供方与消费方之间必须有一份清晰、稳定、可验证的“约定”,这份约定就是服务契约。契约一旦确定,双方就可以并行开发、独立部署,而不会因为内部实现的变化导致调用失败。本节围绕 RESTful API 设计原则、资源建模、版本控制、OpenAPI 规范,以及如何在 ASP.NET Core 中落地契约优先开发展开。
RESTful API 设计原则
REST 不是一种协议,而是一组架构约束。落到 API 设计上,最核心的几条是:以资源为中心、使用统一的接口语义、无状态、可缓存。所谓以资源为中心,是指 URL 应该表示“名词”而不是“动词”。例如,订单服务中获取某个订单,应该设计为 GET /api/orders/{id},而不是 GET /api/getOrderById?id=1。HTTP 方法本身已经表达了动作语义:GET 查询、POST 创建、PUT 全量更新、PATCH 局部更新、DELETE 删除。
统一接口还要求正确使用状态码。200 表示成功,201 表示资源创建成功并通常配合 Location 响应头,204 表示成功但无返回体,400 表示客户端请求有误,401/403 表示未认证或未授权,404 表示资源不存在,409 表示状态冲突,422 表示语义校验失败,500 表示服务端异常。很多团队把所有错误都返回 200,再在响应体里放一个 code 字段,这会让网关、监控、重试策略全部失效,应当避免。
资源建模
资源建模的关键是识别领域中的实体与关系,并决定资源的粒度。以电商场景为例,订单、订单项、支付记录、收货地址都是资源。订单与订单项是聚合关系,订单项不应作为顶层资源单独暴露,而应通过 /api/orders/{orderId}/items 这样的子资源路径访问。这样既表达了从属关系,也避免了订单项脱离订单被误操作。
资源建模还要考虑查询、分页、过滤和排序。列表接口通常设计为 GET /api/orders?status=paid&page=1&pageSize=20&sort=-createdAt。其中 -createdAt 表示按创建时间倒序,这是一种常见约定。分页响应体应包含数据与元信息,例如:
{
"items": [ { "id": "ORD-1001", "status": "paid", "total": 268.00 } ],
"page": 1,
"pageSize": 20,
"total": 137
}
对于复杂查询,不建议把大量条件塞进 URL,而应使用 POST 查询接口,例如 POST /api/orders/search,请求体承载过滤条件。这并不违反 REST,而是对统一接口的合理扩展。
版本控制
服务一旦上线,就会有消费方依赖。此时如果直接修改字段或删除接口,就会造成破坏性变更。版本控制的目的,是让新旧消费方能够在一段时间内共存。常见策略有三种:URL 路径版本、请求头版本、媒体类型版本。URL 路径版本最直观,例如 /api/v1/orders 与 /api/v2/orders;请求头版本通过 api-version: 2.0 传递;媒体类型版本则使用 Accept: application/vnd.contoso.v2+json。
在 ASP.NET Core 中,推荐使用 Asp.Versioning.Mvc 包统一管理版本。下面是一个按 URL 路径分版的配置示例:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersV1Controller : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<OrderDto> Get(string id) => Ok(new OrderDto(id, "paid", 268.00m));
}
版本控制不是越多越好。通常只在发生破坏性变更时升版;新增可选字段、新增接口属于兼容性变更,可以不升版。破坏性变更包括:删除字段、修改字段类型、改变默认行为、收紧校验规则。
OpenAPI 规范
OpenAPI 是一份用 JSON 或 YAML 描述 HTTP API 的规范。它描述路径、方法、参数、请求体、响应体、状态码和数据结构。有了 OpenAPI 文档,消费方可以自动生成客户端 SDK,测试人员可以自动生成用例,网关可以据此做路由与校验。在 ASP.NET Core 中,常用 Swashbuckle.AspNetCore 或 NSwag 生成 OpenAPI 文档。启用方式如下:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
访问 /swagger/v1/swagger.json 即可得到 OpenAPI 文档,访问 /swagger 可以看到交互式 UI。为了让文档准确,控制器和 DTO 上应补充 XML 注释,并开启 GenerateDocumentationFile。
契约优先开发
传统做法是先写代码,再生成文档,这叫“代码优先”。契约优先则相反:先由服务提供方与消费方共同评审并确定 OpenAPI 契约文件,再根据契约生成服务端接口骨架和客户端 SDK。这样做的好处是,契约在编码前就达成一致,减少后期联调返工。
在 ASP.NET Core 中落地契约优先,通常有两种路径。第一种是手写 openapi.yaml,然后用 NSwag.MSBuild 或 Microsoft.OpenApi 工具生成 C# 接口和模型。第二种是使用 Microsoft.AspNetCore.OpenApi 配合 dotnet-openapi 工具,从契约生成最小 API 端点。下面给出一个契约片段示例:
paths:
/api/v1/orders/{id}:
get:
summary: 获取订单详情
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/OrderDto'
'404':
description: 订单不存在
契约确定后,服务端实现必须与契约保持一致。建议在 CI 流水线中加入契约校验步骤:将运行时生成的 OpenAPI 文档与仓库中的契约文件做差异比对,一旦出现未评审的破坏性变更,流水线直接失败。这样契约才真正成为“可执行的约定”,而不是一份过期的文档。
总结来看,服务契约与 API 设计不是“写完接口再补文档”的附属工作,而是微服务协作的起点。RESTful 原则保证接口语义统一,资源建模保证领域边界清晰,版本控制保证演进可控,OpenAPI 保证契约可机器读取,契约优先保证双方在编码前就对接口达成一致。把这五件事串起来,服务之间才能真正做到独立开发、独立部署、稳定协作。