c# - 在 .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 相关?)不允许我以如此简单的方式做到这一点。
我怎样才能做到这一点,所以它不会过于复杂?
解决方案
既然它是一个应该在每个条件之间应用的“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});
推荐阅读
- python - 启动新的房间规划器 Web 应用程序
- cocoapods - NativeScript:在 Pod 示例中工作的 CocoaPod 不会在插件项目中公开符号
- python - AttributeError:“str”对象在尝试使用 pandas from_records 创建数据框时没有属性“keys”
- docker - 在 GitLab 中构建和注册后,Apline Docker Image 未找到 AWS
- macos - 有没有办法在 AppleScript 中获取文件的完整路径?
- python - 使用数组偏移量计算 numpy 中的增量
- java - HibernateProxy.toString 延迟初始化异常
- encryption - Neo4j 服务器证书不受信任
- javascript - 反应引导模式不显示
- python - 如何从不起作用的 pip 安装 mkvirtualenvwrapper?