HotChocolate v13 Attribute式DataLoader实现异常排查求助
近期尝试使用HotChocolate v13的[DataLoader]属性实现数据加载器,参照视频《Let's simplify DataLoader with Hot Chocolate 13》,但遇到问题。使用EF Core 6数据库优先模式,数据库生成的实体模型如下:
namespace dataGraphAPI.Data; public partial class Tenant { public Guid TenantId { get; set; } public string? Name { get; set; } public virtual ICollection<Site> Sites { get; set; } = new List<Site>(); } namespace dataGraphAPI.Data; public partial class Site { public Guid SiteId { get; set; } public Guid? VmsSiteId { get; set; } public string? CmxName { get; set; } public string? Description { get; set; } public Guid? InstanceId { get; set; } public string? TimeZone { get; set; } public int? CmxState { get; set; } public long? CmxStateTime { get; set; } public string? HashProps { get; set; } public Guid? TenantId { get; set; } public virtual ICollection<Host> Hosts { get; set; } = new List<Host>(); public virtual Tenant? Tenant { get; set; } } namespace dataGraphAPI.Data; public partial class Host { public Guid HostId { get; set; } public Guid? SiteId { get; set; } public string? HostName { get; set; } public int? CmxState { get; set; } public long? CmxStateTime { get; set; } public virtual Site? Site { get; set; } }
GraphQL类型类实现
SiteNode 类型定义
using dataGraphAPI.Data; using Microsoft.EntityFrameworkCore; using Host = dataGraphAPI.Data.Host; namespace dataGraphAPI.GraphQL.Types.Sites { [Node] [ExtendObjectType(typeof(Site))] public static class SiteNode { [GraphQLName("hosts")] [BindMember(nameof(Site.Hosts))] [UseOffsetPaging(IncludeTotalCount = true)] [UseFiltering] [UseSorting] internal static async Task<IReadOnlyList<Host?>> GetHostsAsync( [Parent] Site site, ISiteHostsDataLoader dataLoader, CancellationToken ct) => await dataLoader.LoadAsync(site.SiteId.ToString()!, ct); [DataLoader] internal static async Task<ILookup<string, Host>> GetSiteHostsAsync(IReadOnlyList<string> keys, CmxDbContext dbContext, CancellationToken ct) { var sites = await dbContext.Hosts .Where(x => keys.Contains(x.SiteId.ToString()!)) .ToArrayAsync(ct); return sites.ToLookup(x => x.SiteId.ToString()!); } } }
SiteQueries 查询定义
using dataGraphAPI.Data; using Microsoft.EntityFrameworkCore; namespace dataGraphAPI.Types.Sites { [QueryType] public class SiteQueries { [GraphQLName("getSite")] public async Task<Site?> GetSiteById(string tenantId, string siteId, CmxDbContext cmxDbContext, CancellationToken ct) => await cmxDbContext.Sites .FirstOrDefaultAsync(s => s.TenantId.ToString() == tenantId && s.SiteId.ToString() == siteId, ct); [GraphQLName("listSites")] [UseOffsetPaging(IncludeTotalCount = true)] [UseFiltering] [UseSorting] public async Task<IEnumerable<Site?>> GetSitesByTenantIdAsync(string tenantId, CmxDbContext cmxDbContext, CancellationToken ct) => await cmxDbContext.Sites .Where(s => s.TenantId.ToString() == tenantId) .ToListAsync(ct); } }
TenantNode 类型定义
using dataGraphAPI.Data; using Microsoft.EntityFrameworkCore; namespace dataGraphAPI.Types.Tenants { [Node] [ExtendObjectType(typeof(Tenant))] public static class TenantNode { [BindMember(nameof(Tenant.Sites))] [GraphQLName("sites")] [UseOffsetPaging(IncludeTotalCount = true)] [UseFiltering] [UseSorting] internal static async Task<IReadOnlyList<Site?>> GetSitesAsync( [Parent] Tenant tenant, ITenantSitesDataLoader dataLoader, CancellationToken ct) => await dataLoader.LoadAsync(tenant.TenantId.ToString()!, ct); [DataLoader] internal static async Task<ILookup<string, Site>> GetTenantSitesAsync(IReadOnlyList<string> keys, CmxDbContext dbContext, CancellationToken ct) { var sites = await dbContext.Sites .Where(x => keys.Contains(x.TenantId.ToString()!)) .ToArrayAsync(ct); return sites.ToLookup(x => x.TenantId.ToString()!); } } }
TenantQueries 查询定义
using dataGraphAPI.Data; using Microsoft.EntityFrameworkCore; namespace dataGraphAPI.Types.Tenants { [QueryType] public class TenantQueries { [GraphQLName("getTenant")] public async Task<Tenant?> GetTenantById(string tenantId, CmxDbContext cmxDbContext, CancellationToken ct) => await cmxDbContext.Tenants.FirstOrDefaultAsync(s => s.TenantId.ToString() == tenantId, ct); [GraphQLName("listTenants")] [UseOffsetPaging(IncludeTotalCount = true)] [UseFiltering] [UseSorting] public async Task<IEnumerable<Tenant?>> GetTenants(CmxDbContext cmxDbContext, CancellationToken ct) { return await cmxDbContext.Tenants.ToListAsync(ct); } } }
配置文件
Program.cs
using dataGraphAPI.Data; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<CmxDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("CMXContext")) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); builder.Services .AddGraphQLServer() .AddTypes() .AddFiltering() .AddSorting() .RegisterDbContext<CmxDbContext>(); var app = builder.Build(); app.MapGraphQL(); app.Run();
ModuleInfo.cs
[assembly: Module("Types")] [assembly: DataLoaderDefaults(ServiceScope = DataLoaderServiceScope.DataLoaderScope, AccessModifier = DataLoaderAccessModifier.PublicInterface)]
执行的GraphQL查询
query { listTenants{ items { tenantId sites{ instanceId siteId cmxName } } } }
问题现象
执行上述查询时,TenantNode类中的GetSitesAsync方法断点从未触发,且sites字段返回空数组。
问题原因及修复方案
默认导航属性覆盖自定义Resolver
Tenant实体自带Sites导航属性,HotChocolate会自动尝试加载该属性,不会触发你定义的GetSitesAsync方法。[BindMember]注解无法覆盖这一默认行为。- 修复:移除
[BindMember(nameof(Tenant.Sites))],让自定义Resolver接管sites字段的加载逻辑。
- 修复:移除
DataLoader键类型不匹配与不必要的字符串转换
代码中将Guid类型的TenantId转换为string作为DataLoader的键,不仅存在性能损耗,还可能因Guid字符串格式差异(如大小写、大括号)导致匹配失败。- 修复:直接使用Guid作为键类型:
// 修改GetSitesAsync方法 internal static async Task<IReadOnlyList<Site?>> GetSitesAsync( [Parent] Tenant tenant, ITenantSitesDataLoader dataLoader, CancellationToken ct) => await dataLoader.LoadAsync(tenant.TenantId, ct); // 修改DataLoader实现 [DataLoader] internal static async Task<ILookup<Guid, Site>> GetTenantSitesAsync(IReadOnlyList<Guid> keys, CmxDbContext dbContext, CancellationToken ct) { var sites = await dbContext.Sites .Where(x => x.TenantId.HasValue && keys.Contains(x.TenantId.Value)) .ToArrayAsync(ct); return sites.ToLookup(x => x.TenantId.Value); }
- 修复:直接使用Guid作为键类型:
分页注解与返回类型不兼容
GetSitesAsync标注了[UseOffsetPaging]但返回IReadOnlyList<Site?>,分页注解要求返回Connection<Site>类型。- 修复:如果不需要分页,直接移除
[UseOffsetPaging]、[UseFiltering]、[UseSorting]注解;如果需要分页,调整返回类型为Task<Connection<Site>>并适配分页逻辑。
- 修复:如果不需要分页,直接移除
EF查询中的Null安全处理
Site实体的TenantId是可空Guid,直接调用x.TenantId.ToString()会引发空引用异常,需先判断是否为null。(使用Guid作为键时已包含此判断)确认类型扫描范围
确保AddTypes()能扫描到TenantNode类,若命名空间未被自动扫描,可显式指定:builder.Services .AddGraphQLServer() .AddTypes(typeof(TenantNode), typeof(SiteNode)) // ...其他配置
内容的提问来源于stack exchange,提问作者Daniel Ninchev

