【问题标题】:Entity Framework Core nested expressionsEntity Framework Core 嵌套表达式
【发布时间】:2020-03-23 05:38:20
【问题描述】:

在最近发布的 Entity Framework Core 3.0 中,默认为LINQ queries are no longer evaluated on the client。我是这种变化的忠实拥护者,因为它揭示了我的项目中一些潜在危险的客户端评估,我认为这些评估已被转换为 SQL;但是,它也使我用来避免疯狂的三元链的一些辅助方法无法使用。

是否有人设法嵌套 LINQ 表达式以用于 Entity Framework Core 3.0?这是我希望实现的一个示例:

[Fact]
public async Task Can_use_custom_expression()
{
    var dbContext = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseInMemoryDatabase("Test").Options);
    dbContext.Users.Add(new ApplicationUser { FirstName = "Foo", LastName = "Bar" });
    dbContext.SaveChanges();

    string query = "Foo";

    Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);

    var valueCheckFunc = valueCheck.Compile();

    Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);

    var user = await dbContext.Users
        .Where(whereExpression)
        .FirstOrDefaultAsync();

    Assert.NotNull(user);
}

当我运行这个例子时,我得到了以下异常:

Message: 
    System.InvalidOperationException : The LINQ expression 'Where<ApplicationUser>(
        source: DbSet<ApplicationUser>, 
        predicate: (a) => Invoke(__valueCheckFunc_0, a.FirstName, __query_1)
    )' 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 either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

就像我说的,我不想在客户端评估这个表达式,但我想避免在一个表达式中链接十几个 !string.IsNullOrEmpty(x) &amp;&amp; x.Contains(y)。我想要一些关于如何实现这一目标的提示。

【问题讨论】:

    标签: c# linq entity-framework-core entity-framework-core-3.0


    【解决方案1】:

    如果您希望您的表达式可以通过 EF 转换为 Sql,您需要避免调用委托或方法(当然也有一些例外)。但是您想要实现的目标是通过将委托调用替换为其定义表达式来实现。为此,您需要一个专门的 ExpressionVisitor。

    以下访问者将遍历表达式,将其包装调用中的委托引用替换为 lambda 表达式主体:

    public class DelegateByLambda: ExpressionVisitor
    {
        LambdaExpression delegateReferenceExpression;
        LambdaExpression lambdaExpression;
        Stack<InvocationExpression> invocations;
        public DelegateByLambda(LambdaExpression delegateReferenceExpression, LambdaExpression lambdaExpression)
        {
            this.delegateReferenceExpression = delegateReferenceExpression;
            this.lambdaExpression = lambdaExpression;
            this.invocations = new Stack<InvocationExpression>();
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            var paramIndex = lambdaExpression.Parameters.IndexOf(node);
            if (paramIndex >= 0)
            {
                InvocationExpression call = invocations.Peek();
                return base.Visit(call.Arguments[paramIndex]);
            }
            return base.VisitParameter(node);
        }
        protected override Expression VisitInvocation(InvocationExpression node)
        {
            if (node.Expression.ToString() == delegateReferenceExpression.Body.ToString())
            {
                invocations.Push(node);
                var result = base.Visit(lambdaExpression.Body);
                invocations.Pop();
                return result;
            }
            return base.VisitInvocation(node);
        }
    }
    

    此类无法防止尝试使用不匹配的参数(数字和类型)替换委托调用,但是,以下扩展方法可以解决问题:

    public static class DelegateByLambdaExtensions
    {
        public static Expression<T> Replace<T, X>(this Expression<T> source, Expression<Func<X>> delegateReference, Expression<X> lambdaReference)
        {
            return new DelegateByLambda(delegateReference, lambdaReference).Visit(source) as Expression<T>;
        }
    }
    

    因此,您需要在代码中执行的所有操作就是在要转换的表达式上调用 replace 扩展方法,传递一个返回委托的表达式和所需的 lambda 表达式以进行扩展。您的示例应如下所示:

        Expression<Func<string, string, bool>> valueCheck = (value, expected) => !string.IsNullOrEmpty(value) && value.Contains(expected);
    
        var valueCheckFunc = valueCheck.Compile();
    
        Expression<Func<ApplicationUser, bool>> whereExpression = (u) => valueCheckFunc(u.FirstName, query);
        whereExpression = whereExpression.Replace(() => valueCheckFunc, valueCheck);
    
        var user = dbContext.Users
            .Where(whereExpression)
            .FirstOrDefault();
    
        Console.WriteLine(user != null ? $"Found {user.FirstName} {user.LastName}!" : "User not found!");
    

    可以在此处找到工作示例。 https://dotnetfiddle.net/Lun3LA

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-07-24
      • 1970-01-01
      • 2020-05-29
      • 1970-01-01
      • 1970-01-01
      • 2020-06-30
      • 2022-11-02
      相关资源
      最近更新 更多