entity-framework-coregraphqldataloaderhotchocolate

HotChocolate v.13 DataLoader approach with attributes and source generated code not working


I recently started experimenting with HotChocolate v.13. and I am having issues implementing the data loaders with the [DataLoader] attributes like shown in this video: Let's simplify DataLoader with Hot Chocolate 13. I am using EF Core 6 and DB first approach. I have the following data models from my DB:

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

In the "Types" folder I have the following classes:

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

My Program.cs is the following:

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

And my ModuleInfo.cs :

[assembly: Module("Types")]
[assembly: DataLoaderDefaults(ServiceScope = DataLoaderServiceScope.DataLoaderScope, AccessModifier = DataLoaderAccessModifier.PublicInterface)]

So my problem is that when I execute the following GraphQL query:

query {
  listTenants{
    items {
      tenantId
      sites{
        instanceId
        siteId
        cmxName
      }
    }
  }
}

I never hit the breakpoint for GetSitesAsync(...) method in the TenantNode class and the ''sites'' field returns an empty array. Since one tenant has many sites, I thought my Data Loader should be a group one. Obviously I haven't done something right here and I am not sure if I understood correctly the entire idea of the [DataLoader] attributes approach. It is not explained in the HotChocolate documentation and also I could not find any demos with that approach in github. Could anyone please tell me what is wrong with my code?


Solution

  • So I found the mistake - actually it's the access modifier of the GetSitesAsync(...) method in the TenantNode. It should not be internal, it should be public:

        // [BindMember(nameof(Tenant.Sites))] - not necessary
        [GraphQLName("sites")]
        [UseOffsetPaging(IncludeTotalCount = true)]
        [UseFiltering]
        [UseSorting]
    
        public static async Task<IReadOnlyList<Site?>> GetSitesAsync(
        [Parent] Tenant tenant, 
        ITenantSitesDataLoader dataLoader, 
        CancellationToken ct)
        => await dataLoader.LoadAsync(tenant.TenantId.ToString()!, ct);
    

    Everything works fine now with just that small change.