实体框架核心嵌套表达式

时间:2019-11-27 15:56:03

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

在最新版本的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) && x.Contains(y)。我希望获得一些有关实现此目标的提示。

1 个答案:

答案 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);
    }
}

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

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