修改 IQueryable.Include() 的表达式树,为连接添加条件 [英] Modify the expression tree of IQueryable.Include() to add condition to the join

查看:22
本文介绍了修改 IQueryable.Include() 的表达式树,为连接添加条件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

公共抽象类实体{公共 int ID { 获取;放;}公共布尔 IsDeleted { 获取;放;}...}

还有一个存储库:

公共类 BaseStore: IStore其中 TEntity :实体{受保护的只读 ApplicationDbContext 数据库;公共 IQueryable<TEntity>得到所有(){返回 db.Set().Where(e => !e.IsDeleted).InterceptWith(new InjectConditionVisitor(entity => !entity.IsDeleted));}公共 IQueryable<TEntity>GetAll(表达式>谓词){返回 GetAll().Where(谓词);}公共 IQueryable<TEntity>GetAllWithDeleted(){返回 db.Set();}...}

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

IStore 的用法如下:

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

我实现了一个 ExpressionVisitor:

内部类 InjectConditionVisitor;: 表达访客{私有表达式<Func<T, bool>>查询条件;public InjectConditionVisitor(表达式<Func<T, bool>>条件){查询条件 = 条件;}公共覆盖表达式访问(表达式节点){返回base.Visit(节点);}}

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

我看到了一些其他可能有效的解决方案,但它们是永久的",例如 EntityFramework.Filters 似乎适用于大多数用例,但是在配置 DbContext 时必须添加过滤器 - 但是,您可以禁用过滤器,但我不想为每个查询禁用和重新启用过滤器.像这样的另一个解决方案是订阅 ObjectContext 的 ObjectMaterialized 事件,但我也不喜欢它.

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

更新

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

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

 公共类项目:实体{公共字符串名称 { 获取;放;}公共字符串描述 { 获取;放;}公共虚拟ICollection<平台>平台{得到;放;}//注意:此版本不是历史数据,只是项目的版本,如:1.0.0、1.4.2、2.1.0等.公共虚拟ICollection<ProjectVersion>版本 { 获取;放;}}公共类平台:实体{公共字符串名称 { 获取;放;}公共虚拟ICollection<项目>项目{得到;放;}公共虚拟ICollection<TestFunction>测试函数 { 得到;放;}}公共类 ProjectVersion : 实体{公共字符串代码 { 获取;放;}公共虚拟项目项目{获取;放;}}

所以如果我想列出项目的版本,我会调用 store:await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId).我不会删除项目,但如果该项目存在,它会返回所有与之相关的版本,甚至是已删除的版本.在这种特定情况下,我可以从另一边开始并调用 ProjectVersionStore,但如果我想通过 2+ 导航属性进行查询,那么游戏就结束了:)<​​/p>

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

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

解决方案

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

public static IQueryable<T>包括<T, TProperty>(这个IQueryable<T>源,Expression<Func<T, TProperty>>路径){Check.NotNull<IQueryable<T>>(source, "source");Check.NotNull<Expression<Func<T, TProperty>>>(path, "path");字符串路径1;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(值(System.Data.Entity.Core.Objects.Span))

您应该挂钩 VisitMethodCall 来添加您的表达式:

内部类 InjectConditionVisitor;: 表达访客{私有表达式<Func<T, bool>>查询条件;受保护的覆盖表达式访问方法调用(方法调用表达式节点){表达式表达式 = 节点;if (node.Method.Name == "Include" || node.Method.Name == "IncludeSpan"){//在这里做点什么!让我们添加一个 OrderBy 来取乐//拉姆达:x =>x.[物业名称]var 参数 = Expression.Parameter(typeof(T), "x");表达式属性 = Expression.Property(parameter, "ColumnInt");var lambda = Expression.Lambda(属性,参数);//表达式:表达式.[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.Call(null, orderByMethodGeneric, new[] { 表达式, Expression.Quote(lambda) });}别的{表达式 = base.VisitMethodCall(node);}返回表达式;}}

David Fowl 的 QueryInterceptor 项目不支持包含".Entity Framework 尝试使用反射查找Include"方法,如果未找到(就是这种情况)则返回当前查询.

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

我添加了一个支持包含"的 QueryInterceptor 功能来回答您的问题.由于尚未添加单元测试,因此该功能尚不可用,但您可以下载并尝试源代码:查询拦截源

如果您有任何问题,请直接与我联系(电子邮件在我的 GitHub 主页底部),否则这将开始偏离主题.

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

我的项目还包含一个查询过滤器功能,我认为它具有更大的灵活性.

<小时>

从更新的必需项中添加工作示例

这是您可以根据需要使用的起始代码:

public IQueryable得到所有(){var conditionVisitor = new InjectConditionVisitor("版本", 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);内部类 InjectConditionVisitor<T>: 表达访客{私有只读字符串 NavigationString;私有只读 IQueryProvider 提供程序;私有只读函数<IQueryable<T>,IQueryable<T>>查询条件;public InjectConditionVisitor(string navigationString, IQueryProvider provder , Func, IQueryable(表达式);//应用查询条件查询 = 查询条件(查询);//更改查询表达式表达式 = 查询.表达式;}别的{表达式 = base.VisitMethodCall(node);}返回表达式;}}

<小时>

回答子问题

Include 和 IncludeSpan 的区别

据我了解

IncludeSpan:当原始查询尚未被 LINQ 方法修改时出现.

包括:当原始查询已被 LINQ 方法修改时出现(您不再看到以前的表达式)

-- 表达式:{value(System.Data.Entity.Core.Objects.ObjectQuery`1[Z.Test.EntityFramework.Plus.Association_Multi_OneToMany_Left]).MergeAs(AppendOnly).IncludeSpan(value(System.数据.Entity.Core.Objects.Span))}var q = ctx.Association_Multi_OneToMany_Lefts.Include(x => x.Right1s).Include(x => x.Right2s);-- 表达式:{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+ Query IncludeFilter

Basically, I would like to implement a repository that filters all the soft deleted records even through navigation properties. So I have a base entity, something like that:

public abstract class Entity
{
    public int Id { get; set; }

    public bool IsDeleted { get; set; }

    ...
}

And a repository:

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

    ...
}

The InterceptWith function is from this projects: https://github.com/davidfowl/QueryInterceptor and https://github.com/StefH/QueryInterceptor (same with async implementations)

A usage of an IStore<Project> looks like:

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

I implemented an 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);
    }
}

But this is the point where I got stucked. I put a breakpoint in the Visit function to see what expressions I got, and when should I do somthing cheeky, but it never gets to the Include(p => p.Versions) part of my tree.

I saw some other solutions that may work, but those are "permanent", for example EntityFramework.Filters seemed to be good for the most use-cases, but you have to add a filter when you are configuring the DbContext - however, you can disable filters, but I do not want to disable and reenable a filter for every query. Another solution like this is to subscribe the ObjectContext's ObjectMaterialized event but I would not like it either.

My goal would be to "catch" the includes in the visitor and modify the expression tree to add another condition to the join which checks the IsDeleted field of the record only if you use one of the GetAll function of the store. Any help would be appreciated!

Update

The purpose of my repositories is to hide some basic behavior of the base Entity - it also contains "created/lastmodified by", "created/lastmodified-date", timestamp, etc. My BLL gets all the data through this repositories so it does not need to worry about those, the store will handle all the things. There is also a possibility to inherit from the BaseStore for a specific class (then my configured DI will inject to inherited class into IStore<Project> if it exists), where you can add specific behavior. For example if you modify a project, you need to add these modification historical, then you just add this to the update function of the inherited store.

The problem starts when you querying a class that has navigation properties (so any class :D ). There is two concrete entity:

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

So if I would like to list the versions of the project, I call the store: await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId). I will not get deleted project, but if the project exists, it will give back all the Versions related to it, even the deleted ones. In this specific case, I could start from the other side and call the ProjectVersionStore, but if I would like to query through 2+ navigation properties then it's game end:)

The expected behavior would be: if I include the Versions to the Project, it should query only the not deleted Versions - so the generated sql join should contains a [Versions].[IsDeleted] = FALSE condition also. It is even more complicated with complex includes like Include(project => project.Platforms.Select(platform => platform.TestFunctions)).

The reason I'm trying to do it this way is I don't want to refactor all the Include's in the BLL to something else. That's the lazy part:) The another is I would like a transparent solution, I don't want the BLL to know all of this. The interface should be kept unchanged if it is not absolutely necessary. I know it's just an extension method, but this behavior should be in the store layer.

解决方案

The include method you use calls the method QueryableExtensions.Include(source, path1) which transforms the expression into a string path. This is what the include method does:

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

So, your expression looks like this (check the "Include" or "IncludeSpan" method in your expression):

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

You should hook on VisitMethodCall to add your expression instead:

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

The QueryInterceptor project from David Fowl doesn't support "Include". Entity Framework tries to find the "Include" method using reflection and return the current query if not found (which is the case).

Disclaimer: I'm the owner of the project EF+.

I have added a QueryInterceptor feature which supports "Include" to answer your question. The feature is not yet available since unit test has not been added but you can download and try the source: Query Interceptor Source

Contact me directly (email at the bottom of my GitHub homepage) if you have an issue, since this will start to be off topic otherwise.

Be careful, "Include" method modifies the expression by hiding some previous expressions. So it's sometime hard to understand what's really happening under the hood.

My project also contains a Query Filter feature which I believe has more flexibility.


EDIT: Add working example from updated required

Here is a starting code you can use for your requirement:

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


EDIT: Answer sub questions

Difference between Include and IncludeSpan

From what I understand

IncludeSpan: Appears when the original query has not been yet modified by a LINQ method.

Include: Appears when the original query has been modified by a LINQ method (You do not longer see previous expression)

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

How to include and filter related entities

Include does not let you filter related entities. You can find 2 solutions in this post: EF. How to include only some sub results in a model?

  • One involve using a projection
  • One involve using EF+ Query IncludeFilter from my library

这篇关于修改 IQueryable.Include() 的表达式树,为连接添加条件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆