我有以下 GrapqhQL 查询:
query {
listTenants {
totalCount
items {
tenantId
name
sites {
totalCount
items {
siteId
cmxName
cmxState
hosts(
order: { hostId: ASC }
where: { hostName: { neq: "ans" } }
skip: 4
take: 2
) {
totalCount
items {
hostId
hostName
siteId
}
}
}
}
}
}
}
我想对主机对象使用投影 - 我想从数据库中仅提取每个主机的 hostId、hostName 和 siteId。我按如下方式扩展 Site 对象类型:
namespace dataGraphAPI.Types.Nodes
{
[ExtendObjectType<Site>]
public static class SiteNode
{
[GraphQLName(SchemaConstants.Hosts)]
[ListQueries]
public static async Task<IEnumerable<Host>> GetHostsAsync(
[Parent] Site site,
IHostsBySiteIdDataLoader dataLoader,
CancellationToken ct)
=> await dataLoader.LoadAsync(site.SiteId.ToString(), ct);
}
}
我的 [ListQueries] 属性包含以下属性:
using HotChocolate.Types.Descriptors;
using System.Reflection;
namespace dataGraphAPI.Types
{
public sealed class ListQueriesAttribute : ObjectFieldDescriptorAttribute
{
protected override void OnConfigure(IDescriptorContext context, IObjectFieldDescriptor descriptor, MemberInfo member)
{
ApplyAttribute(
context,
descriptor,
member,
new UseOffsetPagingAttribute()
{
IncludeTotalCount = true,
});
ApplyAttribute(
context,
descriptor,
member,
new UseProjectionAttribute());
ApplyAttribute(
context,
descriptor,
member,
new UseFilteringAttribute());
ApplyAttribute(
context,
descriptor,
member,
new UseSortingAttribute());
}
}
}
我的数据加载器如下:
[DataLoader]
internal static async Task<ILookup<string, Host>> GetHostsBySiteIdAsync(IReadOnlyList<string> siteIds, CmxDbContext dbContext, CancellationToken ct)
{
var hosts = dbContext.Hosts
.Where(x => siteIds.Contains(x.SiteId.ToString()));
return hosts.ToLookup(x => x.SiteId.ToString()!);
}
我的Program.cs如下:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<CmxDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("CMXContext"))
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.LogTo(Console.WriteLine, LogLevel.Information));
builder.Services
.AddGraphQLServer()
.AddTypes()
.AddType<AggregateResult>()
.AddType<CountResult>()
.AddType<DistinctResult>()
.AddDirectiveType<AggregateDirectiveType>()
.AddDirectiveType<CountDirectiveType>()
.AddDirectiveType<DistinctDirectiveType>()
.AddFiltering()
.AddSorting()
.AddProjections()
.RegisterDbContext<CmxDbContext>();
var app = builder.Build();
app.MapGraphQL();
app.Run();
问题是,当我从数据库中提取主机时,ListQueriesAttribute 中设置的 UseProjectionAttribute 不起作用,我从数据库中检索每个主机的所有列,而不仅仅是 hostId、hostName 和 siteId。我对数据加载器和 HotChocolate 有点陌生,所以我可能做错了什么。我知道 [UseProjection] 适用于 IQueryable,但在我看来,无法使用数据加载器返回 IQueryable,因此我无法将该属性应用于数据加载器。有什么建议我可以如何通过投影来完成这项工作吗?
所以,我想我现在明白发生了什么,并且找到了问题的解决方案:
这就是我的做法。我知道这不是一个完美的解决方案,但这是我目前唯一的解决方案。我定义一条记录如下:
public record Request
{
public Guid? Id { get; set; }
public IResolverContext ResolverContext { get; set; } = null!;
public override int GetHashCode()
{
return HashCode.Combine(Id);
}
}
我将请求记录作为 key 传递给 dataLoader.LoadAsync(key, CancellationToken) 方法:
[GraphQLName(SchemaConstants.Hosts)]
[ListQueries]
public static async Task<IEnumerable<Host>> GetHostsAsync(
[Parent] Site site,
IResolverContext resolverContext,
IHostsBySiteIdDataLoader dataLoader,
CancellationToken ct)
{
var key = new Request { Id = site.SiteId, ResolverContext = resolverContext };
var result = await dataLoader.LoadAsync(key, ct);
return result;
}
在数据加载器内部,我通过从解析器上下文中提取选择集、过滤子句和排序子句,使用中介方法丰富请求。由于 GraphQL 查询中每个 GraphQL 对象的选择、过滤和排序都是相同的,因此仅从第一个 Request 键获取它们就足够了。然后我将它们作为参数传递给我的 GetRequestedEntities(...) 方法,在该方法中我应用所有必要的投影、过滤和排序:
[DataLoader]
internal static async Task<ILookup<Request, Host>> GetHostsBySiteIdAsync(IReadOnlyList<Request> keys, CMXDbContext dbContext, CancellationToken ct)
{
var parentIds = keys.Select(x => x.Id).ToList();
var requestProps = ResolverHelpers.EnrichRequest(keys[0]);
var entities = dbContext.Hosts.Where(x => parentIds!.Contains(x.SiteId)).AsQueryable();
var list = await ResolverHelpers.GetRequestedEntitiesAsync<Host>(entities, requestProps.Item1, requestProps.Item2, requestProps.Item3, ct);
var result = list.ToLookup(x => keys.Single(k => k.Id == x.SiteId));
return result;
}
为了使其更通用,我使用动态 LINQ。另请注意,为了按照我的方式提取选择集,您应该使用 HotChocolate.PreprocessingExtensions。
using dataGraphAPI.Common;
using HotChocolate.Language;
using HotChocolate.PreProcessingExtensions.Selections;
using Microsoft.EntityFrameworkCore;
using ServiceStack;
using System.Linq.Dynamic.Core;
namespace dataGraphAPI.Types
{
public static class ResolverHelpers
{
public static async Task<IEnumerable<T>> GetRequestedEntitiesAsync<T>(
IQueryable entities,
string selections,
string? filtration,
string? sorting,
CancellationToken cancellationToken) where T : class
{
if (filtration != null)
{
entities = entities.Where(filtration);
}
if (sorting != null)
{
entities = entities.OrderBy(sorting);
}
var result = await entities
.Select<T>($"new {{{selections}}}")
.ToListAsync(cancellationToken);
return result;
}
public static (string, string?, string?) EnrichRequest(Request key)
{
var selections = GetSelections(key);
var filtration = GetFilteringClause(key);
var sorting = GetSortingClause(key);
return (selections, filtration, sorting);
}
private static string GetSelections(Request key)
{
var parent = key.ResolverContext.Parent<Object>().GetType().Name;
var parentId = $"{parent}Id";
var selections = $"{parentId}, {string.Join(", ", key.ResolverContext.GetPreProcessingSelections()!
.Select(s => s.SelectionName)
.Distinct(StringComparer.OrdinalIgnoreCase))}";
return selections;
}
private static string? GetSortingClause(Request key)
{
var sortings = new List<string>();
var orderClause = key.ResolverContext.ArgumentLiteral<IValueNode>(SchemaConstants.Order).Value as IEnumerable<ObjectFieldNode>;
if (orderClause != null)
{
foreach (var order in orderClause)
{
sortings.Add($"{order.Name} {order.Value}");
}
var orderLinq = string.Join(',', sortings);
return orderLinq;
}
else
{
return null;
}
}
private static string? GetFilteringClause(Request key)
{
var filterings = new List<string>();
var whereClause = key.ResolverContext.ArgumentLiteral<IValueNode>(SchemaConstants.Where).Value as IEnumerable<ObjectFieldNode>;
if (whereClause != null)
{
foreach (var filter in whereClause)
{
var filtratingField = filter.Name.ToString();
var input = filter.Value as ObjectValueNode;
foreach (var field in input!.Fields)
{
var name = field.Name.ToString();
var value = field.Value.ToString();
var fieldName = name switch
{
"eq" => $"{filtratingField}=={value}",
"neq" => $"{filtratingField}!={value}",
"gt" => $"{filtratingField}>{value}",
"gte" => $"{filtratingField}>={value}",
"lt" => $"{filtratingField}<{value}",
"lte" => $"{filtratingField}<={value}",
"in" => $"{filtratingField}.Contains({value})",
"nin" => $"!{filtratingField}.Contains({value})",
"startsWith" => $"{filtratingField}.StartsWith({value})",
"nstartsWith" => $"!{filtratingField}.StartsWith({value})",
_ => throw new NotSupportedException()
};
filterings.Add(fieldName);
}
}
var filteringsLinq = string.Join(',', filterings);
return filteringsLinq;
}
else
{
return null;
}
}
}
}
如果有人有更好的建议(可能有,因为我现在是初级开发人员),我希望看到它们,但这就是我现在所拥有的。