【问题标题】:Modify the expression tree of IQueryable.Include() to add condition to the join修改 IQueryable.Include() 的表达式树,为连接添加条件
【发布时间】:2016-01-24 01:58:08
【问题描述】:

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

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/QueryInterceptorhttps://github.com/StefH/QueryInterceptor(与异步实现相同)

IStore&lt;Project&gt; 的用法如下:

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 字段。任何帮助将不胜感激!

更新

我的存储库的目的是隐藏基本实体的一些基本行为 - 它还包含“created/lastmodified by”、“created/lastmodified-date”、时间戳等。我的 BLL 通过这个存储库获取所有数据所以不需要担心这些,商店会处理所有事情。也有可能从 BaseStore 继承特定类(然后我配置的 DI 会将继承的类注入到 IStore&lt;Project&gt; 如果存在),您可以在其中添加特定行为。比如你修改了一个项目,你需要把这些修改历史添加进去,那么你把这个添加到继承存储的更新函数中就可以了。

当您查询具有导航属性的类(因此任何类 :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 =&gt; p.Versions).SingleOrDefaultAsync(p =&gt; p.Id == projectId)。我不会删除项目,但如果项目存在,它将返回所有与之相关的版本,甚至是已删除的版本。在这种特定情况下,我可以从另一边开始并调用 ProjectVersionStore,但如果我想通过 2+ 导航属性进行查询,那么游戏就结束了:)

预期的行为是:如果我将版本包含到项目中,它应该只查询未删除的版本 - 所以生成的 sql 连接也应该包含 [Versions].[IsDeleted] = FALSE 条件。像Include(project =&gt; project.Platforms.Select(platform =&gt; platform.TestFunctions)) 这样的复杂包含更加复杂。

我尝试这样做的原因是我不想将 BLL 中的所有 Include 重构为其他内容。那是懒惰的部分:)另一个是我想要一个透明的解决方案,我不希望 BLL 知道所有这些。如果不是绝对必要,界面应该保持不变。我知道这只是一个扩展方法,但是这个行为应该在 store 层。

【问题讨论】:

    标签: c# .net linq entity-framework-6 expression-trees


    【解决方案1】:

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

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

    因此,您的表达式如下所示(检查表达式中的“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<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 项目不支持“包含”。 Entity Framework 尝试使用反射查找“Include”方法,如果未找到(就是这种情况)则返回当前查询。

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

    我添加了一个支持“包含”的 QueryInterceptor 功能来回答您的问题。由于尚未添加单元测试,因此该功能尚不可用,但您可以下载并尝试源:Query Interceptor Source

    如果您有任何问题,请直接与我联系(电子邮件在我的 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;
        }
    }
    

    编辑:回答子问题

    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. How to include only some sub results in a model?

    • 其中一个涉及使用投影
    • 其中一个涉及使用我的库中的 EF+ Query IncludeFilter

    【讨论】:

    • 嗯,MergeAs 部分很熟悉(我认为它没有说明包含的导航属性 -> 我不知道包含什么以及需要修改什么),但我没有 IncludeSpan。在这种情况下,我的根源似乎是: value(System.Data.Entity.Core.Objects.ObjectQuery`1[TestPlanner.Data.Models.Project]).MergeAs(AppendOnly).Where(e => Not(e. IsDeleted)).SingleOrDefault(p => (p.Id == value(TestPlanner.Web.Services.Projects.ProjectVersionService+c__DisplayClass5_0).projectId)))
    • 您的项目也很有趣,并且有一些非常酷的功能,但我想要一个 100% 透明的解决方案,因为我们无法替换包含过滤器的 BLL 中的每个包含。
    • David Fowl 的 QueryInterceptor 项目不支持“包含”。 Entity Framework 尝试使用反射查找“Include”方法,如果未找到(就是这种情况)则返回当前查询。
    • 酷,这个拦截器工作正常。你能帮我看看 VisitMethodCall 的实际实现吗?我刚刚熟悉了这些表达式树修改以及 EF 如何翻译它们,但这并不容易。它还不是题外话,它是我最初问题的一部分:)非常感谢!
    • 当然可以,我做到了;)您是否尝试通过拦截器过滤包含的相关实体?如果是这样,使用实体框架中的“包含”方法是不可能的。但是,好消息是我可以向您推荐一个替代解决方案。请告诉我这是否正是您想要的预期结果。
    猜你喜欢
    • 2019-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-22
    • 2013-02-24
    • 2016-12-28
    • 1970-01-01
    相关资源
    最近更新 更多