HotChocolate v.13 [UseProjections] 属性不适用于 DataLoaders

问题描述 投票:0回答:1

我有以下 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,因此我无法将该属性应用于数据加载器。有什么建议我可以如何通过投影来完成这项工作吗?

entity-framework-core graphql linq-to-sql dataloader hotchocolate
1个回答
0
投票

所以,我想我现在明白发生了什么,并且找到了问题的解决方案:

  1. HotChocolate 中间件用于投影、过滤和 排序仅对 IQueryable 有效,不适用于 分层设计或 IEnumerable。
  2. 数据加载器始终要求您返回特定类型的结果:分组数据加载器必须始终返回 Task>,批处理数据加载器必须始终返回 Task>。但是,如果您想应用某些投影、过滤和排序,则必须在将 IQueryable 具体化为 IEnumerable 之前执行此操作,以便在数据库内执行所有这些操作。
  3. dataLoader.LoadAsync() 方法不接受 IResolverContext 作为参数,并且无法从内部访问它来收集选择集以及过滤和排序子句。因此,您需要以某种方式将其与密钥一起传递给数据加载器,以便丰富那里的请求。

这就是我的做法。我知道这不是一个完美的解决方案,但这是我目前唯一的解决方案。我定义一条记录如下:

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

如果有人有更好的建议(可能有,因为我现在是初级开发人员),我希望看到它们,但这就是我现在所拥有的。

© www.soinside.com 2019 - 2024. All rights reserved.