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

时间:2016-01-24 01:58:09

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

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

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<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 =&gt; p.Versions)部分。

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

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

更新

我的存储库的目的是隐藏基本实体的一些基本行为 - 它还包含&#34;由&#34;创建/最后修改,&#34;创建/最后修改日期&#34;,时间戳等我的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 join也应该包含[Versions].[IsDeleted] = FALSE条件。复杂的包括Include(project => project.Platforms.Select(platform => platform.TestFunctions))

更复杂

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

1 个答案:

答案 0 :(得分:3)

您使用的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);
}

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

 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项目并不支持&#34; Include&#34;。实体框架试图找到&#34;包含&#34;使用反射的方法,如果没有找到则返回当前查询(这种情况)。

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

我添加了一个QueryInterceptor功能,它支持&#34; Include&#34;回答你的问题。由于尚未添加单元测试但您可以下载并尝试使用源代码,因此该功能尚未推出:Query Interceptor Source

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

小心,&#34;包括&#34;方法通过隐藏一些先前的表达式来修改表达式。因此,有时很难理解幕后真正发生的事情。

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

编辑:从更新后的必需

添加工作示例

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

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
相关问题