1. API设计原则深度实践指南
在现代软件开发中,API(应用程序编程接口)和DLL(动态链接库)是构建复杂系统的基石。一个设计良好的API能够显著提升开发效率,降低维护成本,而糟糕的API设计则可能导致灾难性的后果。本文将深入探讨API设计的核心原则,并通过一个电子商务平台API的完整实现案例,展示如何将这些原则付诸实践。
1.1 契约优先设计:OpenAPI/Swagger完整实现
契约优先设计(Contract-First Design)是一种API开发方法论,它强调在编写任何实现代码之前,首先定义清晰的API规范。这种方法与传统的代码优先(Code-First)方法形成鲜明对比,后者往往导致API设计缺乏一致性,难以维护。
提示:契约优先设计特别适合团队协作场景,它能确保前后端开发人员基于同一份规范工作,减少沟通成本。
1.1.1 完整的OpenAPI 3.0规范定义
OpenAPI规范(原Swagger)是目前最流行的API描述语言,它使用YAML或JSON格式定义API的各个方面。下面是一个电子商务平台API的完整OpenAPI 3.0规范示例:
yaml复制openapi: 3.0.3
info:
title: "电子商务平台API"
description: |
# 电子商务平台 - 完整API规范
## 概述
提供完整的电子商务功能,包括用户管理、产品目录、订单处理、支付集成等。
## 认证
使用Bearer Token进行API认证。获取Token请调用 `/api/v1/auth/login` 端点。
## 版本控制
API版本通过URL路径和Accept头双重控制。
## 速率限制
默认限制:1000次/小时,每个用户。高级用户和企业用户有更高限制。
## 错误处理
所有错误都遵循标准错误格式,包含错误码、消息和详细信息。
version: "1.0.0"
termsOfService: "https://api.example.com/terms"
contact:
name: "API支持团队"
url: "https://support.example.com"
email: "api-support@example.com"
license:
name: "商业许可"
url: "https://api.example.com/license"
x-business-impact: "核心业务API"
x-sla: "99.95%"
x-data-classification: "PII - 需要加密"
externalDocs:
description: "完整的API文档"
url: "https://docs.api.example.com"
servers:
- url: "https://api.example.com/v1"
description: "生产环境"
variables:
environment:
default: "production"
enum:
- "production"
- "staging"
- "development"
region:
default: "us-east-1"
enum:
- "us-east-1"
- "eu-west-1"
- "ap-northeast-1"
- url: "https://staging.api.example.com/v1"
description: "预发布环境"
- url: "https://localhost:5001/v1"
description: "本地开发环境"
tags:
- name: "身份认证"
description: "用户认证和授权相关端点"
externalDocs:
description: "了解更多关于OAuth 2.0"
url: "https://oauth.net/2/"
- name: "用户管理"
description: "用户资料和账户管理"
x-requires-authentication: true
x-required-scopes: ["user:read", "user:write"]
- name: "产品目录"
description: "产品浏览、搜索和管理"
x-cache-ttl: 300
- name: "订单处理"
description: "订单创建、查询和状态更新"
x-requires-authentication: true
x-idempotent: true
- name: "支付集成"
description: "支付处理和退款"
x-sensitive: true
x-pci-dss: true
# 安全方案定义
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
使用JWT令牌进行认证。
格式:`Authorization: Bearer <token>`
令牌可以通过以下方式获取:
1. 用户名/密码登录
2. OAuth 2.0授权码流
3. 刷新令牌
x-token-lifetime: "1小时"
x-refresh-token-lifetime: "30天"
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: "用于服务间通信或机器人的API密钥"
x-rate-limit: "10000次/小时"
OAuth2:
type: oauth2
description: "OAuth 2.0授权"
flows:
authorizationCode:
authorizationUrl: "https://api.example.com/oauth/authorize"
tokenUrl: "https://api.example.com/oauth/token"
refreshUrl: "https://api.example.com/oauth/refresh"
scopes:
user:read: "读取用户信息"
user:write: "修改用户信息"
order:read: "读取订单信息"
order:write: "创建和管理订单"
admin: "管理员权限"
# 路径定义
paths:
/auth/login:
post:
tags:
- "身份认证"
summary: "用户登录"
description: |
使用用户名和密码进行登录,返回访问令牌和刷新令牌。
## 安全注意事项
- 密码必须通过HTTPS传输
- 失败尝试会被记录和限制
- 令牌有明确的过期时间
## 多因素认证
如果用户启用了MFA,第一次登录会返回`requires_mfa: true`,
需要调用 `/auth/mfa/verify` 完成认证。
operationId: "loginUser"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
examples:
standard_login:
summary: "标准登录"
value:
username: "john.doe@example.com"
password: "SecurePass123!"
remember_me: true
mfa_required:
summary: "需要MFA的登录"
value:
username: "admin@example.com"
password: "AdminPass456!"
remember_me: false
responses:
"200":
description: "登录成功"
headers:
X-RateLimit-Limit:
description: "每小时请求限制"
schema:
type: integer
example: 1000
X-RateLimit-Remaining:
description: "剩余请求次数"
schema:
type: integer
example: 999
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
examples:
success:
summary: "登录成功响应"
value:
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
token_type: "Bearer"
expires_in: 3600
refresh_token: "def50200e3b6c4d5f7a8b9c0d1e2f3a4b5c6d7e8f9..."
user:
id: "usr_123456789"
email: "john.doe@example.com"
name: "John Doe"
roles: ["customer"]
mfa_required:
summary: "需要MFA验证"
value:
requires_mfa: true
mfa_methods: ["totp", "sms"]
temporary_token: "temp_123456789"
"400":
description: "请求无效"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
invalid_credentials:
summary: "无效凭证"
value:
error: "invalid_credentials"
message: "用户名或密码不正确"
details:
remaining_attempts: 4
locked_until: null
"429":
description: "请求过于频繁"
headers:
Retry-After:
description: "重试等待时间(秒)"
schema:
type: integer
example: 60
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
security: []
x-audit-log: true
x-skip-rate-limit: false
x-client-ip-header: "X-Forwarded-For"
/users/{userId}:
get:
tags:
- "用户管理"
summary: "获取用户信息"
description: |
根据用户ID获取用户详细信息。
## 权限要求
- 用户只能获取自己的信息,除非有管理员权限
- 敏感字段(如密码哈希、支付信息)不会返回
## 字段过滤
可以使用 `fields` 查询参数指定返回的字段。
operationId: "getUserById"
parameters:
- name: userId
in: path
required: true
schema:
type: string
pattern: "^usr_[a-zA-Z0-9]{9}$"
description: "用户ID,格式:usr_后面跟9位字母数字"
example: "usr_123456789"
- name: fields
in: query
required: false
schema:
type: string
description: |
逗号分隔的字段列表,用于字段过滤。
示例:
- `fields=id,name,email` 只返回这三个字段
- `fields=*` 返回所有字段(默认)
支持的字段:
- id, email, name, phone, address, created_at, updated_at
example: "id,name,email"
- name: If-None-Match
in: header
required: false
schema:
type: string
description: |
ETag值,用于条件请求。
如果提供的ETag与当前资源匹配,返回304 Not Modified。
responses:
"200":
description: "用户信息"
headers:
ETag:
description: "资源的ETag"
schema:
type: string
example: "\"abc123def456\""
Last-Modified:
description: "最后修改时间"
schema:
type: string
format: date-time
example: "2024-01-15T10:30:00Z"
content:
application/json:
schema:
$ref: "#/components/schemas/User"
examples:
customer:
summary: "普通用户"
value:
id: "usr_123456789"
email: "john.doe@example.com"
name: "John Doe"
phone: "+1-555-0123"
address:
street: "123 Main St"
city: "New York"
state: "NY"
country: "USA"
zip_code: "10001"
created_at: "2024-01-01T00:00:00Z"
updated_at: "2024-01-15T10:30:00Z"
admin:
summary: "管理员用户"
value:
id: "usr_admin123"
email: "admin@example.com"
name: "System Admin"
roles: ["admin", "user"]
permissions: ["user:read", "user:write", "order:manage"]
"304":
description: "资源未修改"
"404":
description: "用户不存在"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
security:
- BearerAuth: ["user:read"]
- ApiKeyAuth: []
x-requires-scope: "user:read"
x-cache-control: "private, max-age=60"
put:
tags:
- "用户管理"
summary: "更新用户信息"
description: |
更新用户信息。支持部分更新(PATCH语义)。
## 幂等性
此操作是幂等的,相同的请求多次执行会产生相同的结果。
## 验证规则
- 邮箱格式验证
- 手机号国际格式验证
- 地址格式验证
operationId: "updateUser"
parameters:
- $ref: "#/components/parameters/UserIdParam"
- $ref: "#/components/parameters/IfMatchHeader"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserUpdate"
examples:
basic_update:
summary: "基本信息更新"
value:
name: "John Doe Updated"
phone: "+1-555-0124"
address_update:
summary: "地址更新"
value:
address:
street: "456 Oak Ave"
city: "Los Angeles"
state: "CA"
responses:
"200":
description: "更新成功"
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
$ref: "#/components/responses/ValidationError"
"412":
description: "Precondition Failed - ETag不匹配"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
security:
- BearerAuth: ["user:write"]
x-idempotency-key: true
# 组件定义
components:
# 参数定义
parameters:
UserIdParam:
name: userId
in: path
required: true
schema:
$ref: "#/components/schemas/UserId"
description: "用户唯一标识符"
IfMatchHeader:
name: If-Match
in: header
required: false
schema:
type: string
description: "ETag值,用于乐观并发控制"
PaginationLimit:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: "每页返回的记录数"
PaginationOffset:
name: offset
in: query
required: false
schema:
type: integer
minimum: 0
default: 0
description: "跳过的记录数"
# 响应定义
responses:
ValidationError:
description: "验证错误"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
required_field:
summary: "缺少必需字段"
value:
error: "validation_error"
message: "请求验证失败"
details:
- field: "email"
message: "邮箱不能为空"
invalid_format:
summary: "格式无效"
value:
error: "validation_error"
message: "请求验证失败"
details:
- field: "phone"
message: "手机号格式无效,请使用国际格式"
NotFound:
description: "资源未找到"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Unauthorized:
description: "未授权"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
# 请求体定义
requestBodies:
UserCreateRequest:
description: "创建用户的请求体"
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UserCreate"
# 安全方案
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
# 模式定义
schemas:
# 标识符类型
UserId:
type: string
pattern: "^usr_[a-zA-Z0-9]{9}$"
description: "用户唯一标识符"
example: "usr_123456789"
x-primary-key: true
OrderId:
type: string
pattern: "^ord_[a-zA-Z0-9]{12}$"
description: "订单唯一标识符"
example: "ord_123456789012"
# 认证相关
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
format: email
description: "用户邮箱地址"
example: "user@example.com"
password:
type: string
format: password
description: "用户密码"
minLength: 8
maxLength: 128
example: "SecurePass123!"
remember_me:
type: boolean
description: "是否记住登录状态"
default: false
device_info:
$ref: "#/components/schemas/DeviceInfo"
LoginResponse:
type: object
required:
- access_token
- token_type
- expires_in
properties:
access_token:
type: string
description: "访问令牌"
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
token_type:
type: string
enum: [Bearer]
description: "令牌类型"
expires_in:
type: integer
description: "令牌有效期(秒)"
minimum: 60
maximum: 86400
example: 3600
refresh_token:
type: string
description: "刷新令牌"
example: "def50200e3b6c4d5f7a8b9c0d1e2f3a4b5c6d7e8f9..."
user:
$ref: "#/components/schemas/User"
requires_mfa:
type: boolean
description: "是否需要多因素认证"
default: false
mfa_methods:
type: array
items:
type: string
enum: [totp, sms, email, biometric]
description: "可用的MFA方法"
# 用户相关
User:
type: object
required:
- id
- email
- created_at
properties:
id:
$ref: "#/components/schemas/UserId"
email:
type: string
format: email
description: "用户邮箱"
example: "john.doe@example.com"
name:
type: string
description: "用户全名"
minLength: 2
maxLength: 100
example: "John Doe"
phone:
type: string
description: "手机号码"
pattern: "^\+[1-9]\d{1,14}$"
example: "+15550123"
address:
$ref: "#/components/schemas/Address"
date_of_birth:
type: string
format: date
description: "出生日期"
example: "1990-01-01"
created_at:
type: string
format: date-time
description: "创建时间"
example: "2024-01-01T00:00:00Z"
updated_at:
type: string
format: date-time
description: "最后更新时间"
example: "2024-01-15T10:30:00Z"
last_login_at:
type: string
format: date-time
description: "最后登录时间"
is_active:
type: boolean
description: "账户是否激活"
default: true
is_verified:
type: boolean
description: "邮箱是否验证"
default: false
roles:
type: array
items:
type: string
enum: [customer, merchant, admin]
description: "用户角色"
permissions:
type: array
items:
type: string
description: "用户权限"
preferences:
$ref: "#/components/schemas/UserPreferences"
metadata:
type: object
additionalProperties: true
description: "自定义元数据"
x-sensitive-fields: ["phone", "date_of_birth", "address"]
UserCreate:
allOf:
- $ref: "#/components/schemas/User"
- type: object
required:
- password
properties:
password:
type: string
format: password
minLength: 8
maxLength: 128
pattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"
description: "密码必须包含大小写字母、数字和特殊字符"
confirm_password:
type: string
format: password
description: "确认密码"
terms_accepted:
type: boolean
description: "是否接受服务条款"
default: false
UserUpdate:
type: object
properties:
name:
type: string
minLength: 2
maxLength: 100
phone:
type: string
pattern: "^\+[1-9]\d{1,14}$"
address:
$ref: "#/components/schemas/Address"
preferences:
$ref: "#/components/schemas/UserPreferences"
additionalProperties: false
# 其他组件
Address:
type: object
required:
- street
- city
- country
properties:
street:
type: string
description: "街道地址"
example: "123 Main St"
city:
type: string
description: "城市"
example: "New York"
state:
type: string
description: "州/省"
example: "NY"
country:
type: string
description: "国家代码(ISO 3166-1 alpha-2)"
pattern: "^[A-Z]{2}$"
example: "US"
zip_code:
type: string
description: "邮政编码"
example: "10001"
is_primary:
type: boolean
description: "是否为主要地址"
default: false
UserPreferences:
type: object
properties:
language:
type: string
enum: [en, es, fr, zh, ja]
default: "en"
timezone:
type: string
example: "America/New_York"
currency:
type: string
pattern: "^[A-Z]{3}$"
example: "USD"
email_notifications:
type: boolean
default: true
sms_notifications:
type: boolean
default: false
marketing_emails:
type: boolean
default: false
DeviceInfo:
type: object
properties:
device_id:
type: string
description: "设备唯一标识符"
device_type:
type: string
enum: [web, ios, android, desktop]
os_version:
type: string
app_version:
type: string
user_agent:
type: string
# 错误响应
ErrorResponse:
type: object
required:
- error
- message
properties:
error:
type: string
description: "错误代码"
example: "validation_error"
message:
type: string
description: "错误描述"
example: "请求验证失败"
details:
oneOf:
- type: array
items:
$ref: "#/components/schemas/ValidationErrorDetail"
- type: object
additionalProperties: true
description: "错误详情"
correlation_id:
type: string
description: "请求关联ID,用于问题追踪"
example: "corr_123456789"
timestamp:
type: string
format: date-time
description: "错误发生时间"
example: "2024-01-15T10:30:00Z"
documentation_url:
type: string
format: uri
description: "相关文档链接"
example: "https://docs.api.example.com/errors/validation_error"
retry_after:
type: integer
description: "重试等待时间(秒)"
minimum: 1
maximum: 3600
ValidationErrorDetail:
type: object
required:
- field
- message
properties:
field:
type: string
description: "字段名称"
example: "email"
message:
type: string
description: "错误消息"
example: "邮箱格式无效"
code:
type: string
description: "错误代码"
example: "invalid_format"
value:
description: "提供的值"
constraints:
type: object
additionalProperties: true
# 分页响应
PaginatedResponse:
type: object
required:
- items
- total_count
- limit
- offset
properties:
items:
type: array
description: "当前页的项目列表"
total_count:
type: integer
description: "总项目数"
minimum: 0
example: 125
limit:
type: integer
description: "每页项目数"
minimum: 1
maximum: 100
example: 20
offset:
type: integer
description: "跳过的项目数"
minimum: 0
example: 0
has_more:
type: boolean
description: "是否有更多项目"
links:
$ref: "#/components/schemas/PaginationLinks"
PaginationLinks:
type: object
properties:
self:
type: string
format: uri
description: "当前页链接"
first:
type: string
format: uri
description: "第一页链接"
prev:
type: string
format: uri
description: "上一页链接"
next:
type: string
format: uri
description: "下一页链接"
last:
type: string
format: uri
description: "最后一页链接"
# 扩展字段
x-api-lifecycle: "production"
x-deprecated: false
x-sunset-date: null
x-replacement-api: null
x-roadmap:
- version: "2.0.0"
planned-release: "2024-06-01"
breaking-changes:
- "移除 deprecated 字段"
- "统一错误响应格式"
- "新增 webhook 支持"
x-metrics:
average-response-time: "150ms"
error-rate: "0.05%"
requests-per-day: "1000000"
x-compliance:
gdpr: true
hipaa: false
pci-dss: true
x-integrations:
- name: "Stripe"
type: "payment"
status: "active"
- name: "SendGrid"
type: "email"
status: "active"
这个OpenAPI规范定义了电子商务平台API的各个方面,包括:
- 基本信息:API标题、描述、版本、服务条款等
- 服务器配置:生产环境、预发布环境和本地开发环境的URL
- 标签分类:将API端点按功能分组(身份认证、用户管理等)
- 安全方案:定义了Bearer Token、API Key和OAuth 2.0三种认证方式
- 路径定义:详细描述了每个端点的请求参数、响应格式和错误码
- 组件定义:可重用的参数、响应、请求体和模式定义
- 扩展字段:自定义的业务相关元数据
注意:在实际项目中,OpenAPI规范应该随着API的演进而不断更新,确保文档与实现保持同步。
1.1.2 代码生成与契约驱动开发
有了完整的OpenAPI规范后,我们可以利用各种工具生成客户端代码、服务器存根和文档。下面是一个使用NSwag工具链进行代码生成的C#示例:
csharp复制// 契约优先开发工作流
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using NSwag.CodeGeneration.CSharp;
using NSwag.CodeGeneration.TypeScript;
public class ContractFirstDevelopment
{
// 1. OpenAPI规范验证
public void ValidateOpenApiSpec(string specPath)
{
var openApiDocument = LoadOpenApiDocument(specPath);
// 基础验证
ValidateBasicRequirements(openApiDocument);
// 语义验证
ValidateSemanticRules(openApiDocument);
// 自定义验证规则
ValidateCustomRules(openApiDocument);
}
private void ValidateBasicRequirements(OpenApiDocument document)
{
var errors = new List<string>();
// 检查必需字段
if (string.IsNullOrEmpty(document.Info?.Title))
errors.Add("API标题不能为空");
if (string.IsNullOrEmpty(document.Info?.Version))
errors.Add("API版本不能为空");
// 检查路径
if (!document.Paths.Any())
errors.Add("至少需要一个API路径");
// 检查安全方案
if (!document.Components.SecuritySchemes.Any())
Console.WriteLine("警告:没有定义安全方案");
// 检查操作ID唯一性
var operationIds = new HashSet<string>();
foreach (var path in document.Paths)
{
foreach (var operation in path.Value.Operations)
{
if (string.IsNullOrEmpty(operation.Value.OperationId))
{
errors.Add($"操作缺少OperationId: {operation.Key} {path.Key}");
}
else if (!operationIds.Add(operation.Value.OperationId))
{
errors.Add($"重复的OperationId: {operation.Value.OperationId}");
}
}
}
if (errors.Any())
{
throw new InvalidOperationException($"OpenAPI规范验证失败:\n{string.Join("\n", errors)}");
}
}
private void ValidateSemanticRules(OpenApiDocument document)
{
foreach (var path in document.Paths)
{
foreach (var operation in path.Value.Operations)
{
// 验证HTTP方法使用正确
ValidateHttpMethodUsage(operation.Key, operation.Value);
// 验证请求体
ValidateRequestBody(operation.Value);
// 验证响应
ValidateResponses(operation.Value);
// 验证参数
ValidateParameters(operation.Value);
}
}
}
private void ValidateCustomRules(OpenApiDocument document)
{
// 自定义业务规则验证
var customRules = new[]
{
new ValidationRule
{
Name = "版本前缀检查",
Condition = (doc) => doc.Servers.Any(s => s.Url.Contains("/v1/")),
ErrorMessage = "服务器URL必须包含版本前缀"
},
new ValidationRule
{
Name = "错误响应格式",
Condition = (doc) => doc.Components.Schemas.ContainsKey("ErrorResponse"),
ErrorMessage = "必须定义ErrorResponse模式"
},
new ValidationRule
{
Name = "分页支持",
Condition = (doc) => doc.Paths.Any(p =>
p.Value.Operations.Any(o =>
o.Value.Parameters.Any(param =>
param.Name == "limit" || param.Name == "offset"))),
ErrorMessage = "列表端点应该支持分页"
}
};
foreach (var rule in customRules)
{
if (!rule.Condition(document))
{
Console.WriteLine($"警告: {rule.Name} - {rule.ErrorMessage}");
}
}
}
// 2. 代码生成
public void GenerateClientCode(string specPath, string outputDir)
{
// 生成C#客户端
GenerateCSharpClient(specPath, outputDir);
// 生成TypeScript客户端
GenerateTypeScriptClient(specPath, outputDir);
// 生成Java客户端
GenerateJavaClient(specPath, outputDir);
// 生成Python客户端
GeneratePythonClient(specPath, outputDir);
}
private void GenerateCSharpClient(string specPath, string outputDir)
{
var document = LoadOpenApiDocument(specPath);
var settings = new CSharpClientGeneratorSettings
{
ClassName = "EcommerceApiClient",
GenerateClientClasses = true,
GenerateClientInterfaces = true,
GenerateExceptionClasses = true,
ExceptionClass = "ApiException",
GenerateResponseClasses = true,
GenerateJsonMethods = true,
ParameterDateTimeFormat = "s",
ParameterDateFormat = "yyyy-MM-dd",
GenerateOptionalParameters = true,
GenerateBaseUrlProperty = true,
InjectHttpClient = true,
DisposeHttpClient = false,
UseBaseUrl = true,
UseHttpClientCreationMethod = false,
GeneratePrepareRequestAndProcessResponseAsAsyncMethods = true,
GenerateDtoTypes = true,
GenerateNullableReferenceTypes = true,
CSharpGeneratorSettings =
{
Namespace = "EcommerceApi.Client",
GenerateDataAnnotations = true,
RequiredPropertiesMustBeDefined = true,
ClassStyle = CSharpClassStyle.Poco,
ArrayType = "System.Collections.Generic.List",
ArrayInstanceType = "System.Collections.Generic.List",
DictionaryType = "System.Collections.Generic.Dictionary",
DictionaryInstanceType = "System.Collections.Generic.Dictionary"
}
};
var generator = new CSharpClientGenerator(document, settings);
var code = generator.GenerateFile();
File.WriteAllText(Path.Combine(outputDir, "EcommerceApiClient.cs"), code);
}
private void GenerateTypeScriptClient(string specPath, string outputDir)
{
var document = LoadOpenApiDocument(specPath);
var settings = new TypeScriptClientGeneratorSettings
{
ClassName = "EcommerceApiClient",
GenerateClientClasses = true,
GenerateClientInterfaces = true,
GenerateOptionalParameters = true,
UseTransformOptionsMethod = true,
UseTransformResultMethod = true,
Template = TypeScriptTemplate.Fetch,
PromiseType = PromiseType.Promise,
TypeScriptGeneratorSettings =
{
NullValue = TypeScriptNullValue.Undefined,
GenerateConstructorInterface = true,
TypeStyle = TypeScriptTypeStyle.Interface,
DateTimeType = "Date",
GenerateCloneMethod = false,
MarkOptionalProperties = true
}
};
var generator = new TypeScriptClientGenerator(document, settings);
var code = generator.GenerateFile();
File.WriteAllText(Path.Combine(outputDir, "ecommerce-api-client.ts"), code);
}
private OpenApiDocument LoadOpenApiDocument(string specPath)
{
var fileContents = File.ReadAllText(specPath);
var reader = new OpenApiStringReader();
return reader.Read(fileContents, out var diagnostic);
}
}
public class ValidationRule
{
public string Name { get; set; }
public Func<OpenApiDocument, bool> Condition { get; set; }
public string ErrorMessage { get; set; }
}
这段代码展示了契约优先开发的两个关键步骤:
- OpenAPI规范验证:确保规范符合基本要求、语义规则和自定义业务规则
- 代码生成:根据OpenAPI规范生成多种语言的客户端代码
提示:在实际项目中,应该将代码生成步骤集成到持续集成/持续部署(CI/CD)流程中,确保每次API规范更新时,客户端代码都能自动重新生成。
1.2 API设计核心原则
设计良好的API应该遵循一系列核心原则,这些原则有助于创建易于使用