【问题标题】:Resolve "InvalidOperationException: The LINQ expression [...] could not be translated" and keeping the ToListAsync()解决“InvalidOperationException:无法翻译 LINQ 表达式 [...]”并保留 ToListAsync()
【发布时间】:2020-01-22 03:31:15
【问题描述】:

在 EF Core 3.x 中,无法翻译的 LINQ 查询不再在客户端上进行评估。据我了解,这意味着它无法处理无法直接转换为 SQL 的代码。


就我而言,我想使用 foreach(PropertyInfo in type.GetProperties()) 来评估类的每个组件。

private static bool stringInMovement(Movement m, string toTest)
{
    foreach(PropertyInfo component in typeof(Movement).GetProperties())
    {
        try {
            if (component.GetValue(m).ToString().ToLower().Contains(toTest.ToLower()))
                return true;
        }
        catch { }

    }
    return false;
}

然后在 where 语句中使用它:

movements = movements.Where(m=> stringInMovement(m, SearchString));

我想将结果保留为 IQueryable,因为在它所在的函数之后是异步任务,最后一行是

Movements = await movements.AsNoTracking().ToListAsync();

我想将所有函数保持为异步,并且我不想用 10x m.Component.Contains(searchstring) 编写 where 语句。顺便搜索一下数据库的特定元素。

感谢您的帮助!

我在尝试搜索时遇到的错误:

    Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|8_0(ShapedQueryExpression translated, ref <>c__DisplayClass8_0 )
    Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
    System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
    Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor<TResult>(Expression query)
    Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery<TResult>(Expression query, bool async)
    Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore<TResult>(IDatabase database, Expression query, IModel model, bool async)
    Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler+<>c__DisplayClass12_0<TResult>.<ExecuteAsync>b__0()
    Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore<TFunc>(object cacheKey, Func<Func<QueryContext, TFunc>> compiler)
    Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery<TResult>(object cacheKey, Func<Func<QueryContext, TResult>> compiler)
    Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync<TResult>(Expression query, CancellationToken cancellationToken)
    Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<TResult>.GetAsyncEnumerator(CancellationToken cancellationToken)
    System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable<T>.GetAsyncEnumerator()
    Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync<TSource>(IQueryable<TSource> source, CancellationToken cancellationToken)
    Intuo.IndexModel.OnGetAsync() in Index.cshtml.cs
    +
                Movements = await movements.AsNoTracking().ToListAsync();
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+NonGenericTaskHandlerMethod.Execute(object receiver, object[] arguments)
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
    Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
    Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
    Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
    Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
    Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
    Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
    Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
    Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
    Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

【问题讨论】:

  • 你可以试试 AsEnumerable() 吗?
  • 如果我这样做了,很遗憾我不能使用异步方法

标签: c# asp.net linq asp.net-core ef-core-3.0


【解决方案1】:

您想要的是动态生成一个Expression&lt;Lambda&gt; 以在 where 子句中使用。

        public static Expression<Func<T,bool>> ContainsString<T>(string value)
        {
            var constValue = Expression.Constant(value);
            var parameter = Expression.Parameter(typeof(T), "p");

            return Expression.Lambda<Func<T, bool>>(
                typeof(T).GetProperties()
                    .Where(p => p.PropertyType == typeof(string))
                    .Select(p => (Expression)Expression.Call(
                        Expression.Property(parameter, p), 
                        "Contains", 
                        new Type[] { typeof(string) },
                        constValue))
                    .Aggregate((a, c) => Expression.OrElse(a, c)),
                parameter);
        }

        movements = movements.Where(ContainsString<Movement>(SearchString));

【讨论】:

    【解决方案2】:

    EF/EF Core 无法将涉及反射或更复杂逻辑的任何内容转换为 SQL。取而代之的是,有几种方法可以实现所需 - 在查询中编写所有道具:

    movements = movements
        .Where(m => m.Property1.ToString().ToLower().Contains(SearchString.ToString().ToLower() &&
                    m.Property2.ToString().ToLower().Contains(SearchString.ToString().ToLower() /* ... */ );
    

    或者构建特定的Expression,它将作为参数通过自定义Expression Visitor 或使用方法传递给.Where

    public static Expression<Func<Movement, bool>> CompareToStr(string searchString) 
    {
        Expression res = null;
        var param = Expression.Parameter(typeof(Movement), "x");
        foreach (var component in typeof(Movement).GetProperties())
        {
            // building the expression to get a property
            var arg = Expression.Property(param, component.Name);
            // now we have `x.Property1` expression
    
            var toStrCall = Expression.Call(
                    // to what expression we applying the .ToString method
                    arg,
                    // link to 'ToString', 
                    // needed to be altered, if it would be used in non-sql runtime as if there are nullable types with `null` values, this would cause NRE at runtime
                    component.PropertyType.GetMethod(nameof(object.ToString), new Type[] { }));
            // now we have `x.Property1.ToString()` (watch out NRE)
    
            var toLowerCall = Expression.Call(
                toStrCall, 
                typeof(string).GetMethod(nameof(string.ToLower), new Type[] { }));
    
            // now we have `x.Property1.ToString().ToLower()`
            var containsCall = Expression.Call(
                    toLowerCall,
                    typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) }),
                    Expression.Constant(searchString.ToLower())); // since arguments of expression tree should be the expressions
            // here we passed the constant string expression, so now we have
            // x.Property1.ToString().ToLower().Contains( value of testString.ToLower())
    
            if (res == null)
            {
                res = containsCall;
            }
            else
            {
                res = Expression.Or(res, containsCall);
            }
    
            // after several iterations it has
            // x.Property1...Contains(testString) || x.Property2...Contains(testString) and so on
        }
    
        return Expression.Lambda<Func<Movement, bool>>(res, param);
        // and result x => x.Property1... || x.Property2 ...
    }
    

    (我没有测试过这段代码)。所以目标是创建Expression Tree 的实例并将其作为参数传递给.Where 调用。这样 EF/EF Core 将能够将其翻译成 SQL,因为它知道如何翻译 .ToString() 调用和 .ToLower() 调用:

    movements = movements.Where(CompareToStr(SearchString));
    

    【讨论】:

    • @Matomatt ToString 方法在object 类中定义,因此应该可以在字符串实例上调用ToString,因为字符串继承对象。此外,编辑答案以获得正确 ToString 方法的定义。
    • 非常感谢您的回答!它工作得很好,除非组件是一个字符串,因为它没有“ToString()”方法,它也不适用于布尔值,即使它们有 ToString() 方法。我刚刚添加了一些条件,效果很好,再次感谢您!
    • 应该可以,但是不行¯\_(ツ)_/¯
    猜你喜欢
    • 2021-11-27
    • 1970-01-01
    • 2021-12-28
    • 1970-01-01
    • 2022-12-03
    • 1970-01-01
    • 2022-10-20
    • 2020-09-29
    相关资源
    最近更新 更多