本文介绍了实体框架核心嵌套表达式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

随着Entity Framework Core 3.0的最新发行版,不再在客户端上评估LINQ查询.我非常喜欢这种更改,因为它揭示了我的项目中一些潜在的危险的客户端评估,我认为该评估已转换为SQL.但是,这也使我用来避免疯狂的三元链的一些辅助方法不可用.

With the recent release of Entity Framework Core 3.0, LINQ queries are no longer evaluated on the client by default. I'm a big fan of this change, as it revealed some potentially dangerous client-side evaluation in my project that I thought was translated to SQL; however, it also made some of the helper methods that I was using to avoid crazy chains of ternaries unusable.

有人管理过嵌套LINQ表达式以便与Entity Framework Core 3.0一起使用吗?这是我希望实现的示例:

Has anyone manged to nest LINQ expressions for use with Entity Framework Core 3.0? Here's an example of what I'm hoping to achieve:

[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);
}

运行此示例时,出现以下异常:

When I run this example, I get the following exception:

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)位于单个表达式中.我喜欢一些有关如何实现这一目标的技巧.

Like I said, I do not want to evaluate this expression client-side, but I would like to avoid having to chain a dozen or so !string.IsNullOrEmpty(x) && x.Contains(y)'s in a single expression. I'd love some tips on how to achieve this.

推荐答案

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

If you want your expressions to be translatable to Sql by EF you need to avoid calling delegates or methods (with some exceptions, of course). But what you want to achieve is doable by replacing the delegate invocation with its defining expression. For that you need a specialized ExpressionVisitor.

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

The following visitor will traverse expressions replacing delegate references within its wrapping invocations by a lambda expression body:

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替换委托调用,但是,以下扩展方法可以解决问题:

This class has no protection against attempting to replace delegate invocations by lambdas with mismatching arguments (number and types) however, the following extension method will do the trick:

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表达式进行扩展的表达式.您的示例应如下所示:

So, all you need to do in your code is to call the replace extension method on the expression you want to translate passing an expression returning the delegate and desired lambda expression for expansion. Your sample should look like this:

    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

A working sample can be found here. https://dotnetfiddle.net/Lun3LA

这篇关于实体框架核心嵌套表达式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-15 14:24
查看更多