修改 IQueryable.Include() 的表达式树以向连接添加条件
本文关键字:连接 添加 条件 表达式 Include IQueryable 修改 | 更新日期: 2023-09-27 18:31:33
基本上,我想实现一个存储库,即使通过导航属性也可以过滤所有软删除的记录。所以我有一个基本实体,类似于这样:
public abstract class Entity
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
...
}
还有一个存储库:
public class BaseStore<TEntity> : IStore<TEntity> where TEntity : Entity
{
protected readonly ApplicationDbContext db;
public IQueryable<TEntity> GetAll()
{
return db.Set<TEntity>().Where(e => !e.IsDeleted)
.InterceptWith(new InjectConditionVisitor<Entity>(entity => !entity.IsDeleted));
}
public IQueryable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate)
{
return GetAll().Where(predicate);
}
public IQueryable<TEntity> GetAllWithDeleted()
{
return db.Set<TEntity>();
}
...
}
InterceptWith 函数来自以下项目:https://github.com/davidfowl/QueryInterceptor 和 https://github.com/StefH/QueryInterceptor(与异步实现相同)
IStore<Project>
的用法如下所示:
var project = await ProjectStore.GetAll()
.Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId);
我实现了一个 ExpressionVisitor:
internal class InjectConditionVisitor<T> : ExpressionVisitor
{
private Expression<Func<T, bool>> queryCondition;
public InjectConditionVisitor(Expression<Func<T, bool>> condition)
{
queryCondition = condition;
}
public override Expression Visit(Expression node)
{
return base.Visit(node);
}
}
但这就是我陷入困境的地方。我在 Visit 函数中放置了一个断点,以查看我得到了哪些表达式,以及我什么时候应该做一些厚颜无耻的事情,但它永远不会到达我的树的 Include(p => p.Versions) 部分。
我看到了其他一些可能有效的解决方案,但这些解决方案是"永久的",例如 EntityFramework.Filters 似乎适用于大多数用例,但是您必须在配置 DbContext 时添加一个过滤器 - 但是,您可以禁用过滤器,但我不想为每个查询禁用和重新启用过滤器。另一个这样的解决方案是订阅ObjectContext的ObjectMaterialized事件,但我也不喜欢它。
我的目标是"捕获"访问者中的包含并修改表达式树以向连接添加另一个条件,该条件仅在您使用存储的 GetAll 函数之一时才检查记录的 IsDeleted 字段。任何帮助将不胜感激!
更新
我的仓库的目的是隐藏基本实体的一些基本行为 - 它还包含"创建/上次修改者"、"创建/上次修改日期"、时间戳等。我的 BLL 通过此存储库获取所有数据,因此无需担心这些数据,商店将处理所有事情。还可以从特定类的BaseStore
继承(然后我配置的 DI 会将继承的类注入到IStore<Project>
(如果存在),您可以在其中添加特定行为。例如,如果您修改了一个项目,则需要将这些修改添加到历史修改中,然后只需将其添加到继承存储的更新函数中即可。
查询具有导航属性的类(因此任何类:D)时,问题就开始了。有两个具体的实体:
public class Project : Entity
{
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<Platform> Platforms { get; set; }
//note: this version is not historical data, just the versions of the project, like: 1.0.0, 1.4.2, 2.1.0, etc.
public virtual ICollection<ProjectVersion> Versions { get; set; }
}
public class Platform : Entity
{
public string Name { get; set; }
public virtual ICollection<Project> Projects { get; set; }
public virtual ICollection<TestFunction> TestFunctions { get; set; }
}
public class ProjectVersion : Entity
{
public string Code { get; set; }
public virtual Project Project { get; set; }
}
因此,如果我想列出项目的版本,我会致电商店:await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId)
.我不会删除项目,但如果该项目存在,它将返回与之相关的所有版本,甚至是已删除的版本。在这种特定情况下,我可以从另一端开始并调用ProjectVersionStore,但是如果我想查询2+导航属性,那么游戏结束:)
预期的行为是:如果我将版本包含在项目中,它应该只查询未删除的版本 - 因此生成的 sql 连接也应该包含[Versions].[IsDeleted] = FALSE
条件。对于像Include(project => project.Platforms.Select(platform => platform.TestFunctions))
这样的复杂包含,它甚至更加复杂。
尝试这样做的原因是我不想将 BLL 中的所有包含重构为其他内容。这是懒惰的部分:)另一个是我想要一个透明的解决方案,我不想让 BLL 知道这一切。如果不是绝对必要的,则应保持接口不变。我知道这只是一种扩展方法,但这种行为应该在存储层中。
您使用的 include 方法调用方法 QueryableExtensions.Include(source, path1),它将表达式转换为字符串路径。这是包含方法的作用:
public static IQueryable<T> Include<T, TProperty>(this IQueryable<T> source, Expression<Func<T, TProperty>> path)
{
Check.NotNull<IQueryable<T>>(source, "source");
Check.NotNull<Expression<Func<T, TProperty>>>(path, "path");
string path1;
if (!DbHelpers.TryParsePath(path.Body, out path1) || path1 == null)
throw new ArgumentException(Strings.DbExtensions_InvalidIncludePathExpression, "path");
return QueryableExtensions.Include<T>(source, path1);
}
因此,您的表达式如下所示(检查表达式中的"包含"或"IncludeSpan"方法):
value(System.Data.Entity.Core.Objects.ObjectQuery`1[TEntity]).MergeAs(AppendOnly)
.IncludeSpan(value(System.Data.Entity.Core.Objects.Span))
您应该挂钩 VisitMethodCall 来添加您的表达式:
internal class InjectConditionVisitor<T> : ExpressionVisitor
{
private Expression<Func<T, bool>> queryCondition;
protected override Expression VisitMethodCall(MethodCallExpression node)
{
Expression expression = node;
if (node.Method.Name == "Include" || node.Method.Name == "IncludeSpan")
{
// DO something here! Let just add an OrderBy for fun
// LAMBDA: x => x.[PropertyName]
var parameter = Expression.Parameter(typeof(T), "x");
Expression property = Expression.Property(parameter, "ColumnInt");
var lambda = Expression.Lambda(property, parameter);
// EXPRESSION: expression.[OrderMethod](x => x.[PropertyName])
var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == "OrderBy" && x.GetParameters().Length == 2);
var orderByMethodGeneric = orderByMethod.MakeGenericMethod(typeof(T), property.Type);
expression = Expression.Call(null, orderByMethodGeneric, new[] { expression, Expression.Quote(lambda) });
}
else
{
expression = base.VisitMethodCall(node);
}
return expression;
}
}
David Fowl 的 QueryInterceptor 项目不支持"包含"。实体框架尝试使用反射查找"Include"方法,如果未找到,则返回当前查询(情况确实如此)。
免责声明:我是项目 EF+ 的所有者。
我添加了一个支持"包含"的QueryInterceptor功能来回答您的问题。由于尚未添加单元测试,因此该功能尚不可用,但您可以下载并尝试源代码:查询拦截器源
如果您有问题,请直接与我联系(在我的 GitHub 主页底部发送电子邮件),否则这将开始偏离主题。
请注意,"Include"方法通过隐藏一些以前的表达式来修改表达式。因此,有时很难理解引擎盖下到底发生了什么。
我的项目还包含一个查询过滤器功能,我相信它具有更大的灵活性。
编辑:从更新的所需中添加工作示例
下面是可用于满足要求的起始代码:
public IQueryable<TEntity> GetAll()
{
var conditionVisitor = new InjectConditionVisitor<TEntity>("Versions", db.Set<TEntity>.Provider, x => x.Where(y => !y.IsDeleted));
return db.Set<TEntity>().Where(e => !e.IsDeleted).InterceptWith(conditionVisitor);
}
var project = await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId);
internal class InjectConditionVisitor<T> : ExpressionVisitor
{
private readonly string NavigationString;
private readonly IQueryProvider Provider;
private readonly Func<IQueryable<T>, IQueryable<T>> QueryCondition;
public InjectConditionVisitor(string navigationString, IQueryProvider provder , Func<IQueryable<T>, IQueryable<T>> queryCondition)
{
NavigationString = navigationString;
Provider = provder;
QueryCondition = queryCondition;
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
Expression expression = node;
bool isIncludeSpanValid = false;
if (node.Method.Name == "IncludeSpan")
{
var spanValue = (node.Arguments[0] as ConstantExpression).Value;
// The System.Data.Entity.Core.Objects.Span class and SpanList is internal, let play with reflection!
var spanListProperty = spanValue.GetType().GetProperty("SpanList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var spanList = (IEnumerable)spanListProperty.GetValue(spanValue);
foreach (var span in spanList)
{
var spanNavigationsField = span.GetType().GetField("Navigations", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var spanNavigation = (List<string>)spanNavigationsField.GetValue(span);
if (spanNavigation.Contains(NavigationString))
{
isIncludeSpanValid = true;
break;
}
}
}
if ((node.Method.Name == "Include" && (node.Arguments[0] as ConstantExpression).Value.ToString() == NavigationString)
|| isIncludeSpanValid)
{
// CREATE a query from current expression
var query = Provider.CreateQuery<T>(expression);
// APPLY the query condition
query = QueryCondition(query);
// CHANGE the query expression
expression = query.Expression;
}
else
{
expression = base.VisitMethodCall(node);
}
return expression;
}
}
编辑:回答子问题
包含和包含跨度之间的区别
据我了解
IncludeSpan:当原始查询尚未由 LINQ 方法修改时显示。
包含:当原始查询已被 LINQ 方法修改时显示(您不再看到以前的表达式)
-- Expression: {value(System.Data.Entity.Core.Objects.ObjectQuery`1[Z.Test.EntityFramework.Plus.Association_Multi_OneToMany_Left]).MergeAs(AppendOnly).IncludeSpan(value(System.Data.Entity.Core.Objects.Span))}
var q = ctx.Association_Multi_OneToMany_Lefts.Include(x => x.Right1s).Include(x => x.Right2s);
-- Expression: {value(System.Data.Entity.Core.Objects.ObjectQuery`1[Z.Test.EntityFramework.Plus.Association_Multi_OneToMany_Left]).Include("Right2s")}
var q = ctx.Association_Multi_OneToMany_Lefts.Include(x => x.Right1s).Where(x => x.ColumnInt > 10).Include(x => x.Right2s);
如何包含和筛选相关实体
"包括"不允许您筛选相关实体。您可以在这篇文章中找到 2 个解决方案:EF。如何在模型中仅包含一些子结果?
- 一个涉及使用投影
- 一个涉及使用我的库中的 EF+ 查询包含筛选器