Измените дерево выражений 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 знал обо всем этом. Интерфейс должен быть оставлен без изменений, если это не является абсолютно необходимым. Я знаю, что это просто метод расширения, но это поведение должно быть на уровне магазина.