修改IQueryable.Include()的表达式树以向连接添加条件

基本上,我想实现一个存储库,即使通过导航属性也可以过滤所有软删除的记录。 所以我有一个基本实体,类似的东西:

public abstract class Entity { public int Id { get; set; } public bool IsDeleted { get; set; } ... } 

还有一个存储库:

 public class BaseStore : IStore where TEntity : Entity { protected readonly ApplicationDbContext db; public IQueryable GetAll() { return db.Set().Where(e => !e.IsDeleted) .InterceptWith(new InjectConditionVisitor(entity => !entity.IsDeleted)); } public IQueryable GetAll(Expression<Func> predicate) { return GetAll().Where(predicate); } public IQueryable GetAllWithDeleted() { return db.Set(); } ... } 

InterceptWith函数来自这个项目: https : //github.com/davidfowl/QueryInterceptor和https://github.com/StefH/QueryInterceptor (与异步实现相同)

使用IStore看起来像:

 var project = await ProjectStore.GetAll() .Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId); 

我实现了一个ExpressionVisitor:

 internal class InjectConditionVisitor : ExpressionVisitor { private Expression<Func> queryCondition; public InjectConditionVisitor(Expression<Func> condition) { queryCondition = condition; } public override Expression Visit(Expression node) { return base.Visit(node); } } 

但这就是我被困的地方。 我在Visit函数中放了一个断点来查看我得到了什么表达式,什么时候我应该做一些厚颜无耻的事情,但它永远不会进入我的树的Include(p => p.Versions)部分。

我看到了其他一些可能有效的解决方案,但这些解决方案是“永久性的”,例如EntityFramework.Filters似乎对大多数用例都很好,但是在配置DbContext时必须添加一个filter – 但是,你可以禁用filter,但我不想为每个查询禁用和重新启用filter。 像这样的另一个解决方案是订阅ObjectContext的ObjectMaterialized事件,但我也不喜欢它。

我的目标是“捕获”访问者中的包含并修改表达式树以向连接添加另一个条件,该条件仅在您使用商店的GetAll函数之一时才检查记录的IsDeleted字段。 任何帮助,将不胜感激!

更新

我的存储库的目的是隐藏基本实体的一些基本行为 – 它还包含“created / lastmodified by”,“created / lastmodified-date”,timestamp等。我的BLL通过这个存储库获取所有数据,所以它不用担心那些,商店会处理所有的事情。 还有可能从BaseStoreinheritance特定的类(然后我配置的DI将向inheritance的类注入IStore如果它存在),您可以在其中添加特定的行为。 例如,如果修改项目,则需要添加这些修改历史记录,然后将其添加到inheritance的商店的更新function中。

当您查询具有导航属性的类(因此任何类:D)时,问题就开始了。 有两个具体实体:

  public class Project : Entity { public string Name { get; set; } public string Description { get; set; } public virtual ICollection 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 Versions { get; set; } } public class Platform : Entity { public string Name { get; set; } public virtual ICollection Projects { get; set; } public virtual ICollection 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 join应包含[Versions].[IsDeleted] = FALSE条件。 复杂的包括像Include(project => project.Platforms.Select(platform => platform.TestFunctions))更复杂。

我试图这样做的原因是我不想将BLL中的所有Include重构为其他东西。 这是懒惰的部分:)另一个是我想要一个透明的解决方案,我不希望BLL知道所有这一切。 如果不是绝对必要,接口应保持不变。 我知道它只是一种扩展方法,但这种行为应该在商店层。

您使用的include方法调用QueryableExtensions.Include(source,path1)方法,该方法将表达式转换为字符串路径。 这就是include方法的作用:

 public static IQueryable Include(this IQueryable source, Expression> path) { Check.NotNull>(source, "source"); Check.NotNull>>(path, "path"); string path1; if (!DbHelpers.TryParsePath(path.Body, out path1) || path1 == null) throw new ArgumentException(Strings.DbExtensions_InvalidIncludePathExpression, "path"); return QueryableExtensions.Include(source, path1); } 

所以,你的表达式看起来像这样(检查表达式中的“Include”或“IncludeSpan”方法):

  value(System.Data.Entity.Core.Objects.ObjectQuery`1[TEntity]).MergeAs(AppendOnly) .IncludeSpan(value(System.Data.Entity.Core.Objects.Span)) 

您应该依赖于VisitMethodCall来添加您的表达式:

 internal class InjectConditionVisitor : ExpressionVisitor { private Expression> 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”。 entity framework尝试使用reflection查找“包含”方法,如果未找到则返回当前查询(在这种情况下)。

免责声明 :我是EF +项目的所有者。

我添加了一个QueryInterceptorfunction,它支持“包含”来回答你的问题。 该function尚未提供,因为尚未添加unit testing,但您可以下载并尝试源: 查询拦截器源

如果您遇到问题,请直接与我联系(在我的GitHub主页底部发送电子邮件),否则将开始偏离主题。

注意,“Include”方法通过隐藏一些先前的表达式来修改表达式。 因此,有时很难理解幕后真正发生的事情。

我的项目还包含一个查询filterfunction,我相信它具有更大的灵活性。


编辑:从更新的必填项添加工作示例

以下是您可以根据自己的要求使用的起始代码:

 public IQueryable GetAll() { var conditionVisitor = new InjectConditionVisitor("Versions", db.Set.Provider, x => x.Where(y => !y.IsDeleted)); return db.Set().Where(e => !e.IsDeleted).InterceptWith(conditionVisitor); } var project = await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId); internal class InjectConditionVisitor : ExpressionVisitor { private readonly string NavigationString; private readonly IQueryProvider Provider; private readonly Func, IQueryable> QueryCondition; public InjectConditionVisitor(string navigationString, IQueryProvider provder , Func, IQueryable> 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)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(expression); // APPLY the query condition query = QueryCondition(query); // CHANGE the query expression expression = query.Expression; } else { expression = base.VisitMethodCall(node); } return expression; } } 

编辑:回答子问题

Include和IncludeSpan之间的区别

据我所知

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 + Query IncludeFilter