修改IQueryable.Include()的表达式树以将条件添加到联接中 [英] Modify the expression tree of IQueryable.Include() to add condition to the join

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

问题描述

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

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

    ...
}

还有一个存储库:

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 (与异步实现相同)

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

IStore<Project>的用法如下:

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

我实现了ExpressionVisitor:

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

但这是我被困住的地方.我在Visit函数中设置了一个断点,以查看我得到了什么表达式,什么时候应该做些厚脸皮,但它从未到达树的Include(p => p.Versions)部分.

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.

我看到了一些其他可行的解决方案,但这些解决方案是永久性的",例如 EntityFramework.Filters 在大多数情况下似乎都很好,但是在配置DbContext时必须添加一个过滤器-但是,您可以禁用过滤器,但是我不想为每个查询禁用并重新启用过滤器.这样的另一种解决方案是预订ObjectContext的ObjectMaterialized事件,但我也不希望这样做.

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.

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

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!

更新

我的存储库的目的是隐藏基本实体的一些基本行为-它还包含创建/最后修改者",创建/最后修改日期",时间戳等.我的BLL通过此存储库获取所有数据因此,无需担心这些,商店将处理所有事情.还可以从BaseStore继承特定的类(然后,我配置的DI会将继承的类注入到IStore<Project>(如果存在的话)),您可以在其中添加特定的行为.例如,如果您修改项目,则需要添加这些修改历史记录,然后只需将其添加到继承的商店的更新功能中即可.

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.

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

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

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

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

预期的行为是:如果我将项目的版本包括在内,则它应仅查询未删除的版本-因此生成的sql连接也应包含一个[Versions].[IsDeleted] = FALSE条件.像Include(project => project.Platforms.Select(platform => platform.TestFunctions))这样的复杂包含项,甚至会更加复杂.

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

我尝试以这种方式进行操作的原因是,我不想将BLL中的所有包含项重构为其他内容.那是懒惰的部分:)另一个是我想要一个透明的解决方案,我不希望BLL知道所有这一切.如果不是绝对必要,则该接口应保持不变.我知道这只是一种扩展方法,但是这种行为应该在商店层中.

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.

推荐答案

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

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

因此,您的表达式如下所示(检查表达式中的"Include"或"IncludeSpan"方法):

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

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

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

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

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

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

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

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

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

如果遇到问题,请直接与我联系(在GitHub主页底部的电子邮件),因为否则,这将成为话题.

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.

编辑:从已更新的必需项中添加工作示例

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


编辑:回答子问题


Answer sub questions

包含和IncludeSpan之间的差异

据我了解

IncludeSpan:当尚未通过LINQ方法修改原始查询时显示.

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

包含:当使用LINQ方法修改了原始查询时显示(不再看到以前的表达式)

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

如何包含和过滤相关实体

包含不允许您过滤相关实体.您可以在这篇文章中找到2个解决方案:

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?

  • 涉及使用投影
  • 涉及使用我库中的EF +查询IncludeFilter

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

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