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?
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.