REST 已死:为什么你的 .NET API 应该迁移到 GraphQL

作者:微信公众号:【架构师老卢】
3-1 16:57
26

你是否在犹豫是否要在 .NET 应用中从 REST 转向 GraphQL。我在两种技术领域都有多年经验,在此分享所有心得体会——包括优点、缺点和挑战。


我们将涵盖以下内容

  • GraphQL 的真正定义(通俗易懂)
  • 在 .NET 项目中设置 GraphQL(逐步指南)
  • 与 REST 的真实对比(含代码)
  • 何时使用(以及何时不用)GraphQL
  • 真正重要的性能考量
  • 不影响生产环境的迁移策略

GraphQL 究竟是什么?

基础理解

GraphQL 是一种 API 查询语言,允许客户端按需获取数据。与 REST(服务器决定每个端点返回的数据)不同,GraphQL 让客户端精确指定所需数据。

就像在餐厅点餐——与其接受固定菜单(REST),不如按需定制你的订单(GraphQL)。

类型系统

GraphQL 的核心是强类型系统。每个 GraphQL 服务都定义了一组类型,完整描述可查询的数据。

定义 GraphQL API 时,需先定义这些类型:

  • 对象类型:主数据模型(如 UserOrderProduct
  • 标量类型:基础数据类型(StringIntBoolean 等)
  • 输入类型:用于变更操作的参数类型
  • 枚举:允许的值集合
  • 接口:其他类型可实现的抽象类型

定义类型后,GraphQL 会自动强制执行。你无法请求不存在的字段,且总能获得预期的结果。


操作类型

GraphQL 有三种主要操作类型:

  1. 查询(Queries):获取数据(类似 REST 的 GET)

    • 单次查询可请求多个资源
    • 字段可无限嵌套
    • 始终保持幂等性(不改变数据)
  2. 变更(Mutations):修改数据(类似 REST 的 POST/PUT/DELETE)

    • 单次请求可执行多个修改
    • 返回更新后的数据
    • 顺序执行(不同于查询)
  3. 订阅(Subscriptions):实时更新

    • 与服务器保持活动连接
    • 数据变更时接收更新
    • 适用于聊天应用、实时动态等

REST 与 GraphQL 对比

REST 方式(需多个端点)

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"
    }
}

GraphQL 方式(单次请求,按需获取)

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 会:

  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️⃣ 安装包

dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
  • HotChocolate.AspNetCore
    提供 ASP.NET Core 集成、HTTP 处理、GraphQL 执行管理和模式配置。
  • HotChocolate.Data
    支持过滤、排序、分页和 Entity Framework Core 集成。

2️⃣ 领域模型设计

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; }
}

3️⃣ 创建 GraphQL 类型

注解驱动方式(最简单)

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>();
    }
}

高级 GraphQL 实践

4️⃣ 添加过滤、排序和分页

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
        }
    }
}

N+1 查询问题及解决方案

问题示例

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);
            });
    }
}

解决方案:DataLoader 模式

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 的真实经验,所有代码示例均经过测试并可直接用于生产环境。

相关留言评论
昵称:
邮箱:
阅读排行