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

2024-02-22

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

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/davidfowl/QueryInterceptor and https://github.com/StefH/QueryInterceptor https://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 => p.Versions) 部分。

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

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

Update

我的存储库的目的是隐藏基本实体的一些基本行为 - 它还包含“创建/最后修改日期”、“创建/最后修改日期”、时间戳等。我的 BLL 通过此存储库获取所有数据,因此它确实如此不需要担心这些,商店会处理所有的事情。也有可能继承BaseStore对于特定的类(然后我配置的 DI 将注入到继承的类中IStore<Project>如果存在),您可以在其中添加特定行为。比如你修改了一个项目,你需要添加这些修改历史,那么你只需要把这个添加到继承的store的更新功能中即可。

当您查询具有导航属性的类(因此任何类 :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 连接应该包含[Versions].[IsDeleted] = FALSE状况也。复杂的情况甚至更加复杂,例如Include(project => project.Platforms.Select(platform => platform.TestFunctions)).

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


您使用的 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 项目不支持“包含”。实体框架尝试使用反射查找“包含”方法,如果未找到则返回当前查询(就是这种情况)。

免责声明: 我是该项目的所有者EF+ http://entityframework-plus.net/.

我添加了一个 QueryInterceptor 功能,支持“包含”来回答您的问题。由于尚未添加单元测试,该功能尚不可用,但您可以下载并尝试源代码:查询拦截器源码 https://github.com/zzzprojects/EntityFramework-Plus/tree/master/src/Z.EntityFramework.Plus.EF6/QueryInterceptor

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

请注意,“包含”方法通过隐藏一些先前的表达式来修改表达式。因此,有时很难理解幕后到底发生了什么。

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


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

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

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:回答子问题

Include 和 IncludeSpan 之间的区别

据我了解

IncludeSpan:当原始查询尚未通过 LINQ 方法修改时出现。

Include:当原始查询已被 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。如何在模型中仅包含一些子结果? https://stackoverflow.com/questions/34963319/ef-how-to-include-only-some-sub-results-in-a-model/34971161#34971161

  • 一种涉及使用投影
  • 其中之一涉及使用我的库中的 EF+ Query IncludeFilter
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 操作/Lambda 表达式内存管理问题

    我将一个操作存储在局部变量中 然后在该局部变量超出范围后使用 使用前是否有被清理的危险 这是一个例子 public List GetMaps Action
  • C++:将模板参数的模板类型成员添加为好友的正确语法?

    我有一个带有模板类型参数 tTRAIT 的类 我想加一个模板为好友type member aliastTRAIT 但我无法弄清楚语法 这可能吗 template
  • 无缝滚动瓷砖地图

    我正在开发一个自上而下的角色扮演游戏 并且想要实现无缝滚动地图 也就是说 当玩家探索世界时 地图之间没有加载屏幕 也没有通往下一个区域的 门 我有两种方法可以打破世界 在顶层 我有 区域 它只是 9 个 地图 的集合 这些区域仅由目录表示
  • 有没有办法将 boost::json::serializer 切换为美化输出?

    Using boost json serializer如中的示例所示文档 快速查看 http vinniefalco github io doc json json usage quick look html以紧凑格式保存 json tre
  • C++:初始化静态字符串成员

    我在 C 中初始化静态字符串成员时遇到一些问题 我有几个类 每个类都包含几个表示 id 的静态字符串成员 当我通过调用静态函数初始化变量时 一切都很好 但是 当我想为一个变量分配另一个变量的值时 它仍然保留空字符串 这段代码有什么问题 st
  • 如何填充两个样条线或直线系列之间的区域

    我有这个Chart 如何填充两个之间的区域Series S0 and S1 说蓝色和黄色Series 为此 我们编写了其中之一Paint事件 这里的ValueToPixelPosition https msdn microsoft com
  • ASP.NET MVC 路由 - 向路由添加 .html 扩展名

    我对 MVC 和路由非常陌生 我被要求修改一个应用程序以使用不同的 url 由于我没有经验 这项任务对我来说有点困难 好吧 让我们谈谈一些代码 routes MapRoute CategoryBySeName Route name prod
  • 将一个整数从 C 客户端发送到 Java 服务器

    我使用此代码将一个整数从我的 Java 客户端发送到我的 Java 服务器 int n rand nextInt 50 1 DataOutputStream dos new DataOutputStream socket getOutput
  • 按值返回的函数的返回语句中的初始化

    我的问题源于深入研究std move in return语句 例如以下示例 struct A A std cout lt lt Constructed lt lt this lt lt std endl A A noexcept std c
  • 使用宏计算源文件行数?

    是否可以使用 C C 预处理器将源文件中的行数计算为宏或某种编译时可用值 例如 我可以更换吗MAGIC1 MAGIC2 and MAGIC3在下面 并在使用时以某种方式获取值 4MAGIC3 MAGIC1 can be placed whe
  • ASP.NET MVC 中 ModelState.AddModelError 中的关键参数有什么意义?

    我在我的控制器中添加了验证检查来修改ModelState如果验证失败 例如 private bool ValidateMoney string raw string name decimal min decimal max try var
  • 带有自定义鉴别器的 EntityFramework Code First 继承

    我正在尝试在 EntityFramework Code First 中映射以下继承 public class Member public string ProjectName get set public string AssemblyNa
  • 参数数量在编译时确定的 Lambda 函数

    我想声明一个带有 N 个参数的 lambda 函数 其中 N 是模板参数 就像是 template
  • 未找到 _sqlite3_open 等符号错误

    您好 我收到此错误 Undefined symbols sqlite3 open referenced from main in ccRlWVer o sqliite3 close referenced from main in ccRlW
  • OpenGL 计算着色器调用

    我有一个与新计算着色器相关的问题 我目前正在研究粒子系统 我将所有粒子存储在着色器存储缓冲区中 以便在计算着色器中访问它们 然后我派遣一个一维工作组 define WORK GROUP SIZE 128 shaderManager gt u
  • 如何在 Winform DataGridView 中创建不同的单元格格式

    我有一个 DataGridView 我将其绑定到 DataTable DataTable 是一个全数字值 要求 DataGridView 中的每 n 行都包含文本 而不是数值 以便在视觉上为用户分隔部分 我很高兴在绑定后将此文本数据放入 D
  • 获取会议组织者邮件地址 EWS API

    我想使用 EWS API 获取会议组织者的邮件地址 目前 我刚刚获得约会项目的一些属性 我听说你可以设置你想要获取哪些属性 我的代码看起来像这样 CalendarView cview new CalendarView start end c
  • Java有没有类似微软CHESS的工具?

    是否有类似于 Microsoft 的现有 Java 工具CHESS http research microsoft com chess 或者 CHESS 源代码是否开放 以便我可以尝试将其转换为 Java 谷歌的织线工 http code
  • 如何在没有 Visual Studio 的情况下将新文件添加到 .csproj 文件

    如何添加新文件到 csproj从命令提示符 我认为没有任何工具可以响应命令行上的 add project 命令来执行此操作 但我认为您可以幸运地创建一个程序 脚本来直接操作 csproj 文件的 XML 内容 csproj 文件的结构如下所
  • C# amo 获取角色完整

    我正在开发一个 SSAS 项目 其中除其他事项外 我需要获取 C 中表格多维数据集的完整用户列表 目前我让它以这样的方式工作 我可以获得角色 但数据不完整 当我调用 Server Database Roles 为了便于阅读而简化 属性并枚举

随机推荐