在C#生态中,HotChocolate是目前最成熟的GraphQL服务端库之一。它基于ASP.NET Core构建,提供了从类型定义、查询解析到订阅推送的完整能力。当业务进入高级阶段,我们需要处理字段级授权、批量数据加载以及复杂分页,而不是仅暴露几个基础查询。

一、项目初始化与基础配置
首先通过NuGet安装核心包。HotChocolate的ASP.NET Core集成包已经包含了必要的依赖,我们不需要手动配置底层传输。
// 安装命令(包管理器控制台)
// Install-Package HotChocolate.AspNetCore
// Install-Package HotChocolate.Data
// Install-Package HotChocolate.Authorization
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// 注册GraphQL服务并指定查询类型
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddAuthorization(); // 启用授权扩展
var app = builder.Build();
app.MapGraphQL(); // 默认映射 /graphql 端点
app.Run();
上述代码完成了最小可运行骨架。AddGraphQLServer方法返回一个IRequestExecutorBuilder,所有高级特性都通过该Builder链式开启。AddAuthorization并非ASP.NET Core自带的策略,而是HotChocolate提供的字段级授权基础设施。
与传统的REST控制器不同,GraphQL的解析函数是按需执行的。这意味着我们在Query类中定义的每个字段,都可能在一次请求中被选择性调用,因此授权逻辑必须下沉到字段而非路由层面。
二、使用DataLoader解决N+1查询
假设订单Query需要关联用户信息,若在每个订单解析时单独查库,就会产生典型的N+1问题。HotChocolate的DataLoader通过批处理将多次查询合并为一次IN查询。
using HotChocolate;
using HotChocolate.DataLoader;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class UserByIdDataLoader : BatchDataLoader<int, User>
{
private readonly IDbContextFactory<AppDbContext> _factory;
public UserByIdDataLoader(
IDbContextFactory<AppDbContext> factory,
IBatchScheduler scheduler) : base(scheduler)
{
_factory = factory;
}
protected override async Task<IReadOnlyDictionary<int, User>> LoadBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
using var db = _factory.CreateDbContext();
// 一次查询拿到所有key对应的用户
var users = await db.Users
.Where(u => keys.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, cancellationToken);
return users;
}
}
public class OrderType : ObjectType<Order>
{
protected override void Configure(IObjectTypeDescriptor<Order> descriptor)
{
descriptor.Field(o => o.UserId)
.Ignore(); // 隐藏外键
descriptor.Field<User>("user")
.ResolveWith<OrderResolvers>(r => r.GetUser(default!, default!))
.UseDbContext<AppDbContext>();
}
}
public class OrderResolvers
{
public async Task<User> GetUser(
[Parent] Order order,
UserByIdDataLoader loader)
{
return await loader.LoadAsync(order.UserId);
}
}
BatchDataLoader会在同一请求周期内收集所有待加载的UserId,然后在LoadBatchAsync中统一查询。HotChocolate自动保证同一个DataLoader实例在一次请求中复用,因此不会重复查库。
需要注意,DataLoader只能解决数据库往返次数问题,并不能替代合理的索引设计。如果IN查询涉及大列表,仍应在数据库侧为Id字段建立聚簇索引,否则合并查询反而可能触发全表扫描。
三、字段级JWT角色授权
生产环境中,某些字段仅允许管理员读取。我们可以通过自定义授权策略与HotChocolate的Authorize特性结合实现。
using HotChocolate.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
// 在Program中配置JWT认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://ipipp.com/identity";
options.Audience = "order-api";
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("admin"));
});
// 在查询字段上应用
public class Query
{
[Authorize(Policy = "AdminOnly")]
public IQueryable<Order> GetAllOrders([Service] AppDbContext db)
{
return db.Orders;
}
// 普通用户可访问自身订单
[Authorize]
public IQueryable<Order> GetMyOrders(
[Service] AppDbContext db,
[GlobalState("userId")] int userId)
{
return db.Orders.Where(o => o.UserId == userId);
}
}
HotChocolate在执行解析器前会先运行授权中间件。若策略校验失败,直接返回GraphQL错误而非抛出HTTP 403,这符合GraphQL单一端点、统一错误结构的原则。
GlobalState特性用于从HttpContext注入解析出的声明值。我们可以在认证回调中将Claim中的用户ID写入GraphQL请求上下文,从而避免在每个解析器里重复解析Token。
四、游标分页与过滤排序
对于大数据集,Offset分页会造成深翻页性能衰减。HotChocolate.Data提供了基于Cursor的扩展方法。
using HotChocolate.Data;
using HotChocolate.Data.Sorting;
using HotChocolate.Data.Filters;
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddFiltering() // 启用过滤
.AddSorting() // 启用排序
.AddProjections(); // 启用投影,减少字段查询
public class Query
{
[UsePaging(MaxPageSize = 50)]
[UseFiltering]
[UseSorting]
public IQueryable<Order> GetOrders([Service] AppDbContext db)
{
return db.Orders;
}
}
UsePaging默认使用基于主键的游标编码,客户端传入after参数即可获取下一页。结合UseFiltering,前端能动态构造where条件,而服务端通过表达式树翻译为SQL,避免手动拼接查询。
开启AddProjections后,HotChocolate会分析选择的字段并生成对应的SELECT列,显著降低网络与序列化开销。但投影不能与匿名类型混用,必须保证DbContext能翻译最终表达式。
五、错误拦截与日志
高级API需要统一处理异常,避免内部堆栈泄露到GraphQL响应中。我们可以实现IErrorFilter。
using HotChocolate;
using HotChocolate.Execution;
public class CustomErrorFilter : IErrorFilter
{
public IError OnError(IError error)
{
if (error.Exception is DbUpdateException)
{
return error.WithMessage("数据写入冲突,请稍后重试")
.WithCode("DB_CONFLICT");
}
return error.WithMessage("服务暂时不可用");
}
}
// 注册
builder.Services.AddGraphQLServer()
.AddErrorFilter<CustomErrorFilter>();
该过滤器在错误抛到传输层之前执行,允许我们替换消息、附加扩展字段。结合ASP.NET Core的日志组件,可在OnError中写入结构化日志,方便追踪生产问题。
至此,我们覆盖了HotChocolate构建高级GraphQL API的核心模块:批量加载、字段授权、游标分页与错误处理。将这些机制组合运用,即可支撑高并发、多租户的业务场景。
C#GraphQLHotChocolate修改时间:2026-08-05 16:45:46