你是否在犹豫是否要在 .NET 应用中从 REST 转向 GraphQL。我在两种技术领域都有多年经验,在此分享所有心得体会——包括优点、缺点和挑战。
GraphQL 是一种 API 查询语言,允许客户端按需获取数据。与 REST(服务器决定每个端点返回的数据)不同,GraphQL 让客户端精确指定所需数据。
就像在餐厅点餐——与其接受固定菜单(REST),不如按需定制你的订单(GraphQL)。
GraphQL 的核心是强类型系统。每个 GraphQL 服务都定义了一组类型,完整描述可查询的数据。
定义 GraphQL API 时,需先定义这些类型:
User
、Order
、Product
)String
、Int
、Boolean
等)定义类型后,GraphQL 会自动强制执行。你无法请求不存在的字段,且总能获得预期的结果。
GraphQL 有三种主要操作类型:
查询(Queries):获取数据(类似 REST 的 GET)
变更(Mutations):修改数据(类似 REST 的 POST/PUT/DELETE)
订阅(Subscriptions):实时更新
GET /api/users/123
GET /api/users/123/orders
GET /api/users/123/preferences
需发起三次独立请求,且无论是否需要都会获取所有字段。响应示例如下:
// 第一次请求:/api/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"phoneNumber": "555-0123",
"address": "123 Main St",
"registerDate": "2024-01-01",
"lastLoginDate": "2024-03-15"
}
// 第二次请求:/api/users/123/orders
{
"orders": [
{
"id": 1,
"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"
}
}
query {
user(id: 123) {
name
email
orders {
total
date
}
preferences {
theme
}
}
}
响应仅包含请求的字段:
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com",
"orders": [
{
"total": 99.99,
"date": "2024-03-01"
}
],
"preferences": {
"theme": "dark"
}
}
}
}
当查询到达时,GraphQL 会:
解析器是 GraphQL 执行的核心。它们是负责获取模式中每个字段数据的函数。你可以将其视为“微型端点”,每个端点负责一个特定的数据片段。
这与 REST 有本质区别——在 REST 中,每个端点通常映射到单个控制器操作;而在 GraphQL 中,可能需要数十个解析器协同工作以满足单个查询。
我在一个中型应用(约 130 万条记录)上进行了测试,结果如下:
0️⃣ 简单单资源请求:
1️⃣ 包含关联数据的复杂请求:
2️⃣ 用户配置文件的网络负载:
实话实说——它并非万能:
HotChocolate 是 .NET 中最流行的 GraphQL 服务器,优势包括:
其他选项如 GraphQL.NET 也存在,但 HotChocolate 因与 ASP.NET Core 的深度集成成为事实标准。
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public DateTime OrderDate { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
注解驱动方式(最简单):
public class Query
{
public async Task<User?> GetUser([Service] IUserRepository repository, int id)
{
return await repository.GetUserByIdAsync(id);
}
public async Task<IEnumerable<User>> GetUsers([Service] IUserRepository repository)
{
return await repository.GetUsersAsync();
}
}
类型优先方式(更灵活):
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>();
}
}
public class Query
{
[UsePaging(MaxPageSize = 50)]
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<User> GetUsers([Service] IUserRepository repository)
{
return repository.GetUsers();
}
}
查询示例:
query {
users(
where: {
name: { contains: "John" }
AND: {
orders: { some: { total: { gt: 100 } } }
}
}
order: [
{ name: ASC }
{ email: DESC }
]
first: 10
after: "YXJyYXljb25uZWN0aW9uOjk="
) {
edges {
node {
name
email
orders {
total
orderDate
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor
.Field(f => f.Orders)
.Resolve(async context =>
{
var user = context.Parent<User>();
// 🚫 每个用户触发独立查询!
return await _orderRepository.GetOrdersForUserAsync(user.Id);
});
}
}
public class OrdersByUserDataLoader : BatchDataLoader<int, List<Order>>
{
private readonly IOrderRepository _orderRepository;
public OrdersByUserDataLoader(
IOrderRepository orderRepository,
IBatchScheduler batchScheduler)
: base(batchScheduler)
{
_orderRepository = orderRepository;
}
protected override async Task<IReadOnlyDictionary<int, List<Order>>> LoadBatchAsync(
IReadOnlyList<int> userIds,
CancellationToken cancellationToken)
{
// 单次查询获取所有用户的订单
var allOrders = await _orderRepository.GetOrdersByUserIdsAsync(userIds);
// 按用户 ID 分组返回
return allOrders
.GroupBy(o => o.UserId)
.ToDictionary(g => g.Key, g => g.ToList());
}
}
注册 DataLoader:
public void ConfigureServices(IServiceCollection services)
{
services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddDataLoader<OrdersByUserDataLoader>()
.AddProjections()
.AddFiltering()
.AddSorting();
}
本文基于在 .NET 企业应用中实施 GraphQL 的真实经验,所有代码示例均经过测试并可直接用于生产环境。