我有一个 .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 方法中设置的表达式。
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 无法翻译的重复;我尝试实现该解决方案,但在这种情况下效果不好
我终于找到了解决办法。我不知道是否有更简单的解决方案,但目前这是我找到的唯一解决方案。我希望它可以帮助某人。 我实现了一个拦截器,它在执行选择之前执行 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 和实体属性具有相同名称的情况下才有效),但目前我想我会采用这个解决方案。
您的问题是,您在 GraphQL 查询中有一个复杂的过滤器,您想要从数据库中选择、排序和分页实体。 我通过编写 SQL 过程来按 id 获取结果实体解决了这个问题。这意味着该过程中应用了过滤器的选择、排序和分页。然后,EF-Core 使用 SQL 过程中的 ID 应用过滤器来检索要检索的数据的实体数据。此解决方案有几个优点:
如果您想让 EF-Core 完成所有过滤、排序和分页,那么您必须编写复杂的 linq,而 EF-Core 将很难为其生成有效的 SQL 代码。您面临数据库必须为每个 EF-Core 生成的 SQL 代码生成新执行计划的风险。或者,您最终可能会加载大量数据,只是为了在过滤、排序和分页阶段丢弃大部分数据。
多年来,SQL 数据库一直针对过滤、排序和分页进行优化。 SQL数据库有游标解决方案,可以与EF-Core一起使用。我对 SQL 和 EF-Core 中的游标不太了解。