You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HotChocolate v13 Attribute式DataLoader实现异常排查求助

问题排查:HotChocolate v13 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字段返回空数组。


问题原因及修复方案

  1. 默认导航属性覆盖自定义Resolver
    Tenant实体自带Sites导航属性,HotChocolate会自动尝试加载该属性,不会触发你定义的GetSitesAsync方法。[BindMember]注解无法覆盖这一默认行为。

    • 修复:移除[BindMember(nameof(Tenant.Sites))],让自定义Resolver接管sites字段的加载逻辑。
  2. 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);
      }
      
  3. 分页注解与返回类型不兼容
    GetSitesAsync标注了[UseOffsetPaging]但返回IReadOnlyList<Site?>,分页注解要求返回Connection<Site>类型。

    • 修复:如果不需要分页,直接移除[UseOffsetPaging]、[UseFiltering]、[UseSorting]注解;如果需要分页,调整返回类型为Task<Connection<Site>>并适配分页逻辑。
  4. EF查询中的Null安全处理
    Site实体的TenantId是可空Guid,直接调用x.TenantId.ToString()会引发空引用异常,需先判断是否为null。(使用Guid作为键时已包含此判断)

  5. 确认类型扫描范围
    确保AddTypes()能扫描到TenantNode类,若命名空间未被自动扫描,可显式指定:

    builder.Services
            .AddGraphQLServer()
            .AddTypes(typeof(TenantNode), typeof(SiteNode))
            // ...其他配置
    

内容的提问来源于stack exchange,提问作者Daniel Ninchev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 08:32:02