首页 > 解决方案 > 在 .Where 子句中使用的链接谓词

问题描述

我想创建 EFCore 查询,该查询将返回满足其相关实体某些条件的所有实体。

例如实体看起来像这样(这是非常简化的示例):

public class MyEntity
{
   public int Id { get; set; }
   public List<MyOtherEntity> OtherEntities { get; set; }
}

public class MyOtherEntity
{
   public int Id { get; set; }
   public int SomeProperty1 { get; set; }
   public int SomeProperty2 { get; set; }
}

我有一个采用简化 MyOtherEntity 对象数组的方法:

public class MySimpleOtherEntity
{
   public int SomeProperty1 { get; set; }
   public int SomeProperty2 { get; set; }
}

现在我有一些方法可以获取这些简化对象的 IEnumerable,并且我想返回所有 MyEntity 对象,这些对象在它们的关系中具有匹配所有必需条件的 MyOtherEntities:

public IEnumerable<MyEntity> GetMyEntitiesByMyOtherEntities(IEnumerable<MySimpleOtherEntity> entities)
{
   // example with some static values
   // we want to find all MyEntities that have MyOtherEntity with value 1,2 AND MyOtherEntity with value 2,2
   _dataContext
      .Where(x => x.OtherEntities.Any(y => y.SomeProperty1 == 1 && y.SomeProperty2 == 2)
                  &&
                  x.OtherEntities.Any(y => y.SomeProperty1 == 2 && y.SomeProperty2 == 2)
                  &&
                  .
                  . // and so on
                  .)
      .ToList();

上面的查询已正确转换为 SQL。我已经创建了一个解决方案,将一些原始 SQL 部分粘合在一起,可以提供正确的结果,因为它只是将 AND EXISTS 部分附加到适当的子查询中。

话虽如此,我(如果可能的话)宁愿将它作为一些动态的 LINQ Where 表达式。SQL 解析器创建的 SQL 几乎与我为本示例所做的一样好,但是对于原始 SQL 查询,我失去了 EFCore 给我的一些控制。

我创建了一些谓词列表,我想将它们链接在一起并注入 .Where:

public IEnumerable<MyEntity> GetMyEntitiesByMyOtherEntities(IEnumerable<MySimpleOtherEntity> entities)
{
    var predicates = new List<Expression<Func<MyEntity, bool>>>();

    foreach(var entity in entities)
    {
       predicates.Add(x => x.OtherEntities.Any(y => y.SomeProperty1 == entity.SomeProperty1 
                                                    && y.SomeProperty2 == entity.SomeProperty2);
    }

}

不幸的是,我不知道如何正确链接它们。我试着用

var combinedPredicate = predicates.Aggregate((l, r) => Expression.AndAlso(l, r));

但它有一些铸造问题(可能与 AndAlso 返回 BinaryExpression 相关?)不允许我以如此简单的方式做到这一点。

我怎样才能做到这一点,所以它不会过于复杂?

标签: c#entity-framework-coreexpression-trees

解决方案


既然它是一个应该在每个条件之间应用的“And”,为什么你不多次使用“Where”?

var predicates = ...
var myElements = ...
foreach(var predicate in predicate) 
{
   myElements = myElements.Where(predicate);
}

您尝试使用表达式进行的聚合可能会起作用,但会更复杂一些。

编辑这里是你如何通过聚合表达式来做到这一点:

        var param = predicates.First().Parameters.First();
        var body = predicates.Select(s => s.Body).Aggregate(Expression.AndAlso);
        var lambda = (Expression<Func<Temp, bool>>)Expression.Lambda(body, param);

所以第一部分的代码并不难。假设您有两个谓词:

t => t.Value < 10;
t => t.Value > 5;

第一个参数将被保留(t,我稍后会解释原因)。然后我们提取表达式的主体,所以我们得到:

t.Value < 10;
t.Value > 5;

然后我们用 "And" 聚合它们:

t.Value < 10 && t.Value > 5

然后我们再次创建一个 lambda:

t => t.Value < 10 && t.Value > 5

所以一切看起来都很好,但如果你尝试编译它,你会得到一个错误。为什么?一切看起来都很好。这是因为开头的“t”和第二个条件中的“t”不一样......它们的名称相同但来自不同的表达式(因此创建了不同的对象,名称不足以相同)他们是一样的...)。

为了解决每次使用参数时都需要检查以将其替换为相同的值

您需要实现一个“访问者”(来自访问者模式),它将检查整个表达式以替换参数的使用:

public static class ExpressionHelper
{

    public static Expression<Func<T, bool>> ReplaceParameters<T>(this Expression<Func<T, bool>> expression, ParameterExpression param)
    {
        return (Expression<Func<T, bool>>)new ReplaceVisitor<T>(param).Modify(expression);
    }

    private class ReplaceVisitor<T> : ExpressionVisitor
    {
        private readonly ParameterExpression _param;

        public ReplaceVisitor(ParameterExpression param)
        {
            _param = param;
        }

        public Expression Modify(Expression expression)
        {
            return Visit(expression);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node.Type == typeof(T) ? _param : node;
        }
    }
}

这个实现是幼稚的,肯定有很多缺陷,但在这样的基本情况下,我认为这已经足够了。

然后您可以通过将此行添加到第一个代码块来使用它:

lambda = lambda.ReplaceParameters(param);

现在您可以将它与 EF 一起使用......甚至可以用于内存中的对象:

var result = lambda.Compile()(new Temp() {Value = 5});

推荐阅读