REST 已死:为什么你的 .NET API 应该迁移到 GraphQL
你是否在犹豫是否要在 .NET 应用中从 REST 转向 GraphQL。我在两种技术领域都有多年经验,在此分享所有心得体会——包括优点、缺点和挑战。
我们将涵盖以下内容
- • GraphQL 的真正定义(通俗易懂)
- • 在 .NET 项目中设置 GraphQL(逐步指南)
- • 与 REST 的真实对比(含代码)
- • 何时使用(以及何时不用)GraphQL
- • 真正重要的性能考量
- • 不影响生产环境的迁移策略
GraphQL 究竟是什么?
基础理解
GraphQL 是一种 API 查询语言,允许客户端按需获取数据。与 REST(服务器决定每个端点返回的数据)不同,GraphQL 让客户端精确指定所需数据。
就像在餐厅点餐——与其接受固定菜单(REST),不如按需定制你的订单(GraphQL)。
类型系统
GraphQL 的核心是强类型系统。每个 GraphQL 服务都定义了一组类型,完整描述可查询的数据。
定义 GraphQL API 时,需先定义这些类型:
- • 对象类型:主数据模型(如
User
、Order
、Product
) - • 标量类型:基础数据类型(
String
、Int
、Boolean
等) - • 输入类型:用于变更操作的参数类型
- • 枚举:允许的值集合
- • 接口:其他类型可实现的抽象类型
定义类型后,GraphQL 会自动强制执行。你无法请求不存在的字段,且总能获得预期的结果。
操作类型
GraphQL 有三种主要操作类型:
- 1. 查询(Queries):获取数据(类似 REST 的 GET)
- • 单次查询可请求多个资源
- • 字段可无限嵌套
- • 始终保持幂等性(不改变数据)
- 2. 变更(Mutations):修改数据(类似 REST 的 POST/PUT/DELETE)
- • 单次请求可执行多个修改
- • 返回更新后的数据
- • 顺序执行(不同于查询)
- 3. 订阅(Subscriptions):实时更新
- • 与服务器保持活动连接
- • 数据变更时接收更新
- • 适用于聊天应用、实时动态等
REST 与 GraphQL 对比
REST 方式(需多个端点)
代码语言:javascript代码运行次数:0运行复制GET /api/users/
GET /api/users//orders
GET /api/users//preferences
需发起三次独立请求,且无论是否需要都会获取所有字段。响应示例如下:
代码语言:javascript代码运行次数:0运行复制// 第一次请求:/api/users/123
{
"id":,
"name":"John Doe",
"email":"john@example",
"phoneNumber":"555-0123",
"address":"123 Main St",
"registerDate":"2024-01-01",
"lastLoginDate":"2024-03-15"
}
// 第二次请求:/api/users/123/orders
{
"orders":[
{
"id":,
"date":"2024-03-01",
"total":99.99,
"items":[...],
"shippingAddress":"...",
"billingAddress":"...",
"status":"delivered"
}
]
}
// 第三次请求:/api/users/123/preferences
{
"preferences":{
"theme":"dark",
"emailNotifications":true,
"language":"en",
"timezone":"UTC-5"
}
}
GraphQL 方式(单次请求,按需获取)
代码语言:javascript代码运行次数:0运行复制query {
user(id: ) {
name
email
orders {
total
date
}
preferences {
theme
}
}
}
响应仅包含请求的字段:
代码语言:javascript代码运行次数:0运行复制{
"data":{
"user":{
"name":"John Doe",
"email":"john@example",
"orders":[
{
"total":99.99,
"date":"2024-03-01"
}
],
"preferences":{
"theme":"dark"
}
}
}
}
GraphQL 执行流程
当查询到达时,GraphQL 会:
- 1. 解析查询以理解请求的字段
- 2. 将每个字段匹配到对应的解析器(Resolver)
- 3. 尽可能并行执行解析器
- 4. 将结果组装成请求的精确结构
解析器是 GraphQL 执行的核心。它们是负责获取模式中每个字段数据的函数。你可以将其视为“微型端点”,每个端点负责一个特定的数据片段。
这与 REST 有本质区别——在 REST 中,每个端点通常映射到单个控制器操作;而在 GraphQL 中,可能需要数十个解析器协同工作以满足单个查询。
性能考量
我在一个中型应用(约 130 万条记录)上进行了测试,结果如下:
0️⃣ 简单单资源请求:
- • REST:45ms
- • GraphQL:48ms(简单请求略有开销)
1️⃣ 包含关联数据的复杂请求:
- • REST(多端点):320ms
- • GraphQL(单次请求):89ms(GraphQL 优势明显!)
2️⃣ 用户配置文件的网络负载:
- • REST:24KB(完整用户对象)
- • GraphQL:8KB(仅请求的字段)
何时不应使用 GraphQL?
实话实说——它并非万能:
- 1. 简单 CRUD 应用 如果只是构建基础管理面板,REST 可能更简单。
- 2. 文件上传 GraphQL 虽支持,但比 REST 复杂。
- 3. 小团队且工期紧张 学习曲线可能对小型项目不划算。
在 .NET 中设置 GraphQL(逐步指南)
0️⃣ 生态
HotChocolate 是 .NET 中最流行的 GraphQL 服务器,优势包括:
- • 专为 .NET 构建
- • 高性能
- • 丰富功能集
- • 活跃社区
- • 定期更新
其他选项如 GraphQL.NET 也存在,但 HotChocolate 因与 ASP.NET Core 的深度集成成为事实标准。
1️⃣ 安装包
代码语言:javascript代码运行次数:0运行复制dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
- • HotChocolate.AspNetCore: 提供 ASP.NET Core 集成、HTTP 处理、GraphQL 执行管理和模式配置。
- • HotChocolate.Data: 支持过滤、排序、分页和 Entity Framework Core 集成。
2️⃣ 领域模型设计
代码语言:javascript代码运行次数:0运行复制public classUser
{
publicint Id { get; set; }
publicstring Name { get; set; }
publicstring Email { get; set; }
public List<Order> Orders { get; set; }
}
publicclassOrder
{
publicint Id { get; set; }
publicdecimal Total { get; set; }
public DateTime OrderDate { get; set; }
publicint UserId { get; set; }
public User User { get; set; }
}
3️⃣ 创建 GraphQL 类型
注解驱动方式(最简单):
代码语言:javascript代码运行次数:0运行复制public classQuery
{
publicasync Task<User?> GetUser([Service] IUserRepository repository, int id)
{
returnawait repository.GetUserByIdAsync(id);
}
publicasync Task<IEnumerable<User>> GetUsers([Service] IUserRepository repository)
{
returnawait repository.GetUsersAsync();
}
}
类型优先方式(更灵活):
代码语言:javascript代码运行次数:0运行复制public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(f => f.Id).Type<NonNullType<IdType>>();
descriptor.Field(f => f.Name).Type<NonNullType<StringType>>();
descriptor.Field(f => f.Email).Type<NonNullType<StringType>>();
descriptor
.Field(f => f.Orders)
.ResolveWith<UserResolvers>(r => r.GetOrders(default!, default!))
.UseDbContext<AppDbContext>();
}
}
高级 GraphQL 实践
4️⃣ 添加过滤、排序和分页
代码语言:javascript代码运行次数:0运行复制public class Query
{
[UsePaging(MaxPageSize = 50)]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<User> GetUsers([Service] IUserRepository repository)
{
return repository.GetUsers();
}
}
查询示例:
代码语言:javascript代码运行次数:0运行复制query {
users(
where:{
name:{contains:"John"}
AND:{
orders:{some:{total:{gt:}}}
}
}
order:[
{name: ASC }
{email: DESC }
]
first:
after:"YXJyYXljb25uZWN0aW9uOjk="
){
edges {
node {
name
email
orders {
total
orderDate
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
N+1 查询问题及解决方案
问题示例
代码语言:javascript代码运行次数:0运行复制public classUserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor
.Field(f => f.Orders)
.Resolve(async context =>
{
var user = context.Parent<User>();
//