LINQ 表达式 OrderBy DateTimeOffset.DateTime 无法翻译

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

我有一个 .NET Core 项目,我在其中实现了这个 GraphQL 查询。查询工作正常,但按日期排序不起作用。

[UsePaging(MaxPageSize = 200, IncludeTotalCount = true)]
[UseProjection]
[UseFiltering]
[UseSorting]
public async Task<IQueryable<UserDto>> UserMetadatas(string myValue, [Service] ActivityEngineDataContext context,[Service] IMapper mapper)
{
  var entities = context.Users.Where(act => act.MyValue== myValue);
  return entities.ProjectTo<UserDto>(mapper.ConfigurationProvider);
}

使用 AutoMapper,我创建了以下投影来使用 DTO 映射实体。

CreateProjection<User, UserDto>()
  .ForMember(dest => dest.CreationDate, opt => opt.MapFrom(s => s.CreationDate.DateTime))
  .ForMember(dest => dest.LastUpdateDate, opt => opt.MapFrom(s => s.LastUpdateDate.DateTime));

我必须创建这个投影,因为 User 类的属性是 DateTimeOffset 类型,而 UserDto 类的属性是 DateTime 类型。

当我从 Banana Cake Pop 游乐场运行此查询时

query{
  activityInstancesMetadata(
    myValue: "value"
    where: {and: [{status: {eq: 32}}]}
    order: {lastUpdateDate: ASC}
    first: 20
  ) {
    nodes {
      creationDate
      lastUpdateDate
      id
      status
    }
    totalCount
  }
}

我有这个错误:

"The LINQ expression 'DbSet<User>()
.Where(a => a.MyValue== __myValue_0)
.OrderBy(a => a.LastUpdateDate.DateTime)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'

at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|15_0(ShapedQueryExpression translated, <>c__DisplayClass15_0& )
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
   at HotChocolate.Types.Pagination.QueryableCursorPagingHandler`1.ResolveAsync(IResolverContext context, IQueryable`1 source, CursorPagingArguments arguments, CancellationToken cancellationToken)
   at HotChocolate.Types.Pagination.CursorPagingHandler.HotChocolate.Types.Pagination.IPagingHandler.SliceAsync(IResolverContext context, Object source)
   at HotChocolate.Types.Pagination.PagingMiddleware.InvokeAsync(IMiddlewareContext context)
   at HotChocolate.Utilities.MiddlewareCompiler`1.ExpressionHelper.AwaitTaskHelper(Task task)
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"

阅读堆栈跟踪和调试,问题似乎是由实体框架引起的,实体框架在 OrderBy 方法中插入了 Projection 的 ForMember 方法中设置的表达式。

到目前为止我已经尝试过的事情

  • 在DataContext中设置转换,这样
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  foreach (var entityType in modelBuilder.Model.GetEntityTypes())
  {
    var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)
                                                                || p.PropertyType == typeof(DateTimeOffset?));
    foreach (var property in properties)
    {
      modelBuilder
          .Entity(entityType.Name)
          .Property(property.Name)
          .HasConversion(new DateTimeOffsetToStringConverter());
    }
  }
}
  • 投影中的双重投射
CreateProjection<User, UserDto>()
  .ForMember(dest => dest.CreationDate, opt => opt.MapFrom(s => (DateTime)(object)s.CreationDate))
  .ForMember(dest => dest.LastUpdateDate, opt => opt.MapFrom(s => (DateTime)(object)s.LastUpdateDate));

此代码有效,但当我调用返回单个记录的查询时,我遇到其他问题。

有没有办法在查询中不使用“ToList”的情况下解决此问题?

注意:这不是 EF Core 查询仅 DateTime of DateTimeOffset 无法翻译的重复;我尝试实现该解决方案,但在这种情况下效果不好

c# entity-framework linq graphql automapper
2个回答
0
投票

我终于找到了解决办法。我不知道是否有更简单的解决方案,但目前这是我找到的唯一解决方案。我希望它可以帮助某人。 我实现了一个拦截器,它在执行选择之前执行 order by ,这样排序是在实体上而不是在 dto 上进行的。因此,实体框架不使用映射的 .ForMember() 中指定的表达式来执行排序。

using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace MyProject.Common
{
  public class OrderByDateTimeOffsetQueryInterceptor : IQueryExpressionInterceptor
  {
    private static readonly MethodInfo _orderByMethod = typeof(Queryable).GetMethod(
      name: nameof(Queryable.OrderBy),
      bindingAttr: BindingFlags.Static | BindingFlags.Public,
      types: new[]
      {
        typeof(IQueryable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)),
        typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(
          Type.MakeGenericMethodParameter(0),
          Type.MakeGenericMethodParameter(1)))
      })!;

    private static readonly MethodInfo _orderByDescendingMethod = typeof(Queryable).GetMethod(
      name: nameof(Queryable.OrderByDescending),
      bindingAttr: BindingFlags.Static | BindingFlags.Public,
      types: new[]
      {
        typeof(IQueryable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)),
        typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(
          Type.MakeGenericMethodParameter(0),
          Type.MakeGenericMethodParameter(1)))
      })!;

    private readonly ILogger<OrderByDateTimeOffsetQueryInterceptor> _logger;

    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
    {
      return OrderByExpressionVisitor.Instance.Visit(queryExpression);
    }


    private class OrderByExpressionVisitor : ExpressionVisitor
    {
      public static readonly OrderByExpressionVisitor Instance = new();

      protected override Expression VisitMethodCall(MethodCallExpression node)
      {
        if (!node.Method.IsGenericMethod)
          return base.VisitMethodCall(node);

        if (node.Method.GetGenericMethodDefinition() != _orderByMethod && node.Method.GetGenericMethodDefinition() != _orderByDescendingMethod)
          return base.VisitMethodCall(node);

        Debug.Assert(node.Arguments[1].NodeType is ExpressionType.Quote);
        LambdaExpression keySelector = (LambdaExpression)((UnaryExpression)node.Arguments[1]).Operand;

        if (keySelector.Body is not MemberExpression { Member: PropertyInfo property } || property.PropertyType != typeof(DateTime))
          return base.VisitMethodCall(node);

        return new SelectExpressionVisitor(property.Name, node.Method.GetGenericMethodDefinition()).Visit(node.Arguments[0]);
      }
    }

    private class SelectExpressionVisitor : ExpressionVisitor
    {
      private readonly string _propertyName;
      private readonly MethodInfo _methodInfo;

      public SelectExpressionVisitor(string propertyName, MethodInfo method)
      {
        _propertyName = propertyName;
        _methodInfo = method;
      }

      protected override Expression VisitMethodCall(MethodCallExpression node)
      {
        if (node.Method.Name != nameof(Queryable.Select))
          return base.VisitMethodCall(node);

        Type sourceType = node.Arguments[0].Type.GetGenericArguments()[0];
        PropertyInfo? dateTimeOffsetProperty = sourceType.GetProperty(
          name: _propertyName,
          bindingAttr: BindingFlags.Instance | BindingFlags.Public,
          binder: null,
          returnType: typeof(DateTimeOffset),
          types: Type.EmptyTypes,
          modifiers: null);

        if (dateTimeOffsetProperty is null)
          return base.VisitMethodCall(node);

        ParameterExpression sourceParam = Expression.Parameter(sourceType, "source");
        LambdaExpression keySelector = Expression.Lambda(
          body: Expression.Property(
            expression: sourceParam,
            propertyName: _propertyName),
          parameters: sourceParam);

        return node.Update(node.Object, new Expression[]
        {
          Expression.Call(
            instance: null,
            method: _methodInfo.MakeGenericMethod(sourceType, typeof(DateTimeOffset)),
            arg0: node.Arguments[0],
            arg1: Expression.Quote(keySelector)),
          node.Arguments[1]
        });
      }
    }
  }
}

代码可以改进,也许可以通过注入 AutoMapper(目前它仅在 DTO 和实体属性具有相同名称的情况下才有效),但目前我想我会采用这个解决方案。


0
投票

您的问题是,您在 GraphQL 查询中有一个复杂的过滤器,您想要从数据库中选择、排序和分页实体。 我通过编写 SQL 过程来按 id 获取结果实体解决了这个问题。这意味着该过程中应用了过滤器的选择、排序和分页。然后,EF-Core 使用 SQL 过程中的 ID 应用过滤器来检索要检索的数据的实体数据。此解决方案有几个优点:

  1. 仅从数据库中检索相关实体,
  2. EF-Core 做了它最擅长的事情,您可以使用 linq 来丰富结果,
  3. 没有巨大的数据开销和大的内存消耗开销,
  4. 由于分页位于堆栈底部,GraphQL 适用于有限的实体列表,
  5. 当 SQL 过程编码和优化良好并且具有良好的执行计划时,响应速度更快。注意:您对数据库有两次调用,
  6. C# 代码和数据库之间有一个额外的接口层,
  7. 一切尽在您的掌控之中。

如果您想让 EF-Core 完成所有过滤、排序和分页,那么您必须编写复杂的 linq,而 EF-Core 将很难为其生成有效的 SQL 代码。您面临数据库必须为每个 EF-Core 生成的 SQL 代码生成新执行计划的风险。或者,您最终可能会加载大量数据,只是为了在过滤、排序和分页阶段丢弃大部分数据。

多年来,SQL 数据库一直针对过滤、排序和分页进行优化。 SQL数据库有游标解决方案,可以与EF-Core一起使用。我对 SQL 和 EF-Core 中的游标不太了解。

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