Измените дерево выражений IQueryable.Include (), чтобы добавить условие в объединение

По сути, я хотел бы реализовать репозиторий, который фильтрует все мягко удаленные записи даже через свойства навигации. Итак, у меня есть базовая сущность, что-то вроде этого:

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 (то же самое с асинхронными реализациями)

Использование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);
    }
}

Но это тот момент, когда я застрял. Я поместил точку останова в функцию «Посетить», чтобы увидеть, какие выражения я получил и когда мне следует делать что-то дерзкое, но оно никогда не попадает в часть «Включить» (p => p.Versions) моего дерева.

Я видел некоторые другие решения, которые могут работать, но они, например, «постоянные»EntityFramework.Filters Казалось бы, подходит для большинства случаев использования, но вы должны добавить фильтр при настройке DbContext - однако вы можете отключить фильтры, но я не хочу отключать и повторно включать фильтр для каждого запроса. Другое решение, подобное этому, состоит в том, чтобы подписаться на событие ObjectContext ObjectMaterialized, но мне бы это тоже не понравилось.

Моя цель состоит в том, чтобы «перехватить» включения в посетителе и изменить дерево выражений, чтобы добавить еще одно условие в объединение, которое проверяет поле IsDeleted записи, только если вы используете одну из функций GetAll хранилища. Любая помощь будет оценена!

Обновить

Цель моих репозиториев состоит в том, чтобы скрыть некоторое базовое поведение базовой сущности - она ​​также содержит «созданный / последний измененный», «созданный / последний измененный-дата», отметку времени и т. Д. Мой 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 должно содержать[Versions].[IsDeleted] = FALSE состояние тоже. Еще сложнее со сложными включениями вродеInclude(project => project.Platforms.Select(platform => platform.TestFunctions)).

Причина, по которой я пытаюсь сделать это таким образом, заключается в том, что я не хочу рефакторировать все Включить в BLL на что-то другое. Это ленивая часть :) Другое - я бы хотел прозрачного решения, я не хочу, чтобы BLL знал обо всем этом. Интерфейс должен быть оставлен без изменений, если это не является абсолютно необходимым. Я знаю, что это просто метод расширения, но это поведение должно быть на уровне магазина.

Ответы на вопрос(1)

Ваш ответ на вопрос