微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 .Where 子句中使用的链式谓词

如何解决在 .Where 子句中使用的链式谓词

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

例如实体看起来像这样(这是一个非常简单的例子):

sum

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

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

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

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

以上查询已正确转换为 sql。我已经创建了一个解决方案,通过粘合一些原始 sql 部分来提供正确的结果,因为它只是将 AND EXISTS 部分与适当的子查询连接起来。

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

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

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();

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

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

}

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

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

解决方法

既然应该在每个条件之间应用“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;

然后我们用“和”来聚合它们:

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。