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; }
...
}
And a repository:
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>();
}
...
}
The InterceptWith function is from this projects: https://github.com/davidfowl/QueryInterceptor and https://github.com/StefH/QueryInterceptor (same with async implementations)
A usage of an IStore<Project>
looks like:
var project = await ProjectStore.GetAll()
.Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId);
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);
}
}
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.
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.
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!
Update
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.
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; }
}
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:)
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))
.
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.