I have a framework which implements Soft Deletes in the Database (Nullable DateTime called DeletedDate). I am using a Repository to handle the main entity requests like so:
/// <summary>
/// Returns a Linq Queryable instance of the entity collection.
/// </summary>
public IQueryable<T> All
{
get { return Context.Set<T>().Where(e => e.DeletedDate == null); }
}
This works great, but the issue I'm having is when you include navigational properties, and how to make sure only Active records are queried. The repository method in question starts like this:
/// <summary>
/// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded.
/// </summary>
/// <param name="includeProperties">Connected objects to be included in the result set.</param>
/// <returns>An IQueryable collection of entity.</returns>
public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null);
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query;
}
So if the Repository is being used by an entity called Parent which has a navigational property called Children, the AllIncluding method would properly filter out the soft deleted Parent records, but the soft deleted Children records would still be included.
Looking at the query sent to the database, it seems all that needs to be done is add to the sql join clause " AND Children.DeletedDate IS NULL " and the query would return the correct results.
During my research, I found the this post which appears to be exactly what I need, however my implementation doesn't get the same results the poster had. Stepping through the code, nothing seems to happen to the Children part of the query.
Here is my current relevant code (Note: Using QueryInterceptor from nuget):
BaseClass:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DomainClasses
{
/// <summary>
/// Serves as the Base Class for All Data Model Classes
/// </summary>
public class BaseClass
{
/// <summary>
/// Default constructor, sets EntityState to Unchanged.
/// </summary>
public BaseClass()
{
this.StateOfEntity = DomainClasses.StateOfEntity.Unchanged;
}
/// <summary>
/// Indicates the current state of the entity. Not mapped to Database.
/// </summary>
[NotMapped]
public StateOfEntity StateOfEntity { get; set; }
/// <summary>
/// The entity primary key.
/// </summary>
[Key, Column(Order = 0), ScaffoldColumn(false)]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// The date the entity record was created. Updated in InsightDb.SaveChanges() method
/// </summary>
[Column(Order = 1, TypeName = "datetime2"), ScaffoldColumn(false)]
public DateTime AddDate { get; set; }
/// <summary>
/// The UserName of the User who created the entity record. Updated in InsightDb.SaveChanges() method
/// </summary>
[StringLength(56), Column(Order = 2), ScaffoldColumn(false)]
public string AddUser { get; set; }
/// <summary>
/// The date the entity record was modified. Updated in InsightDb.SaveChanges() method
/// </summary>
[Column(Order = 3, TypeName = "datetime2"), ScaffoldColumn(false)]
public DateTime ModDate { get; set; }
/// <summary>
/// The UserName of the User who modified the entity record.
/// </summary>
[StringLength(56), Column(Order = 4), ScaffoldColumn(false)]
public string ModUser { get; set; }
/// <summary>
/// Allows for Soft Delete of records.
/// </summary>
[Column(Order = 5, TypeName = "datetime2"), ScaffoldColumn(false)]
public DateTime? DeletedDate { get; set; }
}
}
Parent Class:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace DomainClasses
{
/// <summary>
/// The Parent Entity.
/// </summary>
public class Parent : BaseClass
{
/// <summary>
/// Instantiates a new instance of Parent, initializes the virtual sets.
/// </summary>
public Parent()
{
this.Children = new HashSet<Child>();
}
#region Properties
/// <summary>
/// The Parent's Name
/// </summary>
[StringLength(50), Required, Display(Name="Parent Name")]
public string Name { get; set; }
#endregion
#region Relationships
/// <summary>
/// Relationship to Child, 1 Parent = Many Children.
/// </summary>
public virtual ICollection<Child> Children { get; set; }
#endregion
}
}
Child Class:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DomainClasses
{
/// <summary>
/// The Child entity. One Parent = Many Children
/// </summary>
public class Child : BaseClass
{
#region Properties
/// <summary>
/// Child Name.
/// </summary>
[Required, StringLength(50), Display(Name="Child Name")]
public string Name { get; set; }
#endregion
#region Relationships
/// <summary>
/// Parent Relationship. 1 Parent = Many Children.
/// </summary>
public virtual Parent Parent { get; set; }
#endregion
}
}
Context Class:
using DomainClasses;
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
namespace DataLayer
{
public class DemoContext : DbContext, IDemoContext
{
/// <summary>
/// ActiveSession object of the user performing the action.
/// </summary>
public ActiveSession ActiveSession { get; private set; }
public DemoContext(ActiveSession activeSession)
: base("name=DemoDb")
{
ActiveSession = activeSession;
this.Configuration.LazyLoadingEnabled = false;
}
#region Db Mappings
public IDbSet<Child> Children { get; set; }
public IDbSet<Parent> Parents { get; set; }
#endregion
public override int SaveChanges()
{
var changeSet = ChangeTracker.Entries<BaseClass>();
if (changeSet != null)
{
foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged))
{
entry.Entity.ModDate = DateTime.UtcNow;
entry.Entity.ModUser = ActiveSession.UserName;
if (entry.State == EntityState.Added)
{
entry.Entity.AddDate = DateTime.UtcNow;
entry.Entity.AddUser = ActiveSession.UserName;
}
else if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.DeletedDate = DateTime.UtcNow;
}
}
}
return base.SaveChanges();
}
public new IDbSet<T> Set<T>() where T : BaseClass
{
return ((DbContext)this).Set<T>();
}
}
}
Repository Class:
using DomainClasses;
using QueryInterceptor;
using System;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
namespace DataLayer
{
/// <summary>
/// Entity Repository to be used in Business Layer.
/// </summary>
public class EntityRepository<T> : IEntityRepository<T> where T : BaseClass
{
public IDemoContext Context { get; private set; }
/// <summary>
/// Main Constructor for Repository. Creates an instance of DemoContext (derives from DbContext).
/// </summary>
/// <param name="activeSession">UserName of the User performing the action.</param>
public EntityRepository(ActiveSession activeSession)
: this(new DemoContext(activeSession))
{
}
/// <summary>
/// Constructor for Repository. Allows a context (i.e. FakeDemoContext) to be passed in for testing.
/// </summary>
/// <param name="context">IDemoContext to be used in the repository. I.e. FakeDemoContext.</param>
public EntityRepository(IDemoContext context)
{
Context = context;
}
/// <summary>
/// Returns a Linq Queryable instance of the entity collection.
/// </summary>
public IQueryable<T> All
{
get { return Context.Set<T>().Where(e => e.DeletedDate == null); }
}
/// <summary>
/// Returns a Linq Queryable instance of the entity collection, allowing connected objects to be loaded.
/// </summary>
/// <param name="includeProperties">Connected objects to be included in the result set.</param>
/// <returns>An IQueryable collection of entity.</returns>
public IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = Context.Set<T>().Where(e => e.DeletedDate == null);
InjectConditionVisitor icv = new InjectConditionVisitor();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query.InterceptWith(icv);
}
/// <summary>
/// Finds a single instance of the entity by the Id.
/// </summary>
/// <param name="id">The primary key for the entity.</param>
/// <returns>An instance of the entity.</returns>
public T Find(int id)
{
return Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id);
}
/// <summary>
/// Takes a single entity or entity graph and reads the explicit state, then applies the necessary State changes to Update or Add the entities.
/// </summary>
/// <param name="entity">The entity object.</param>
public void InsertOrUpdate(T entity)
{
if (entity.StateOfEntity == StateOfEntity.Added)
{
Context.Set<T>().Add(entity);
}
else
{
Context.Set<T>().Add(entity);
Context.ApplyStateChanges();
}
}
/// <summary>
/// Deletes the instance of the entity.
/// </summary>
/// <param name="id">The primary key of the entity.</param>
public void Delete(int id)
{
var entity = Context.Set<T>().Where(e => e.DeletedDate == null).SingleOrDefault(e => e.Id == id);
entity.StateOfEntity = StateOfEntity.Deleted;
Context.Set<T>().Remove(entity);
}
/// <summary>
/// Saves the transaction.
/// </summary>
public void Save()
{
Context.SaveChanges();
}
/// <summary>
/// Disposes the Repository.
/// </summary>
public void Dispose()
{
Context.Dispose();
}
}
}
InjectConditionVisitor Class:
using System;
using System.Linq;
using System.Linq.Expressions;
namespace DataLayer
{
public class InjectConditionVisitor : ExpressionVisitor
{
private QueryConditional queryCondition;
public InjectConditionVisitor(QueryConditional condition)
{
queryCondition = condition;
}
public InjectConditionVisitor()
{
queryCondition = new QueryConditional(x => x.DeletedDate == null);
}
protected override Expression VisitMember(MemberExpression ex)
{
// Only change generic types = Navigation Properties
// else just execute the normal code.
return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(queryCondition, ex) ?? base.VisitMember(ex);
}
/// <summary>
/// Create the where expression with the adapted QueryConditional
/// </summary>
/// <param name="condition">The condition to use</param>
/// <param name="ex">The MemberExpression we're visiting</param>
/// <returns></returns>
private Expression CreateWhereExpression(QueryConditional condition, Expression ex)
{
var type = ex.Type;//.GetGenericArguments().First();
var test = CreateExpression(condition, type);
if (test == null)
return null;
var listType = typeof(IQueryable<>).MakeGenericType(type);
return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType);
}
/// <summary>
/// Adapt a QueryConditional to the member we're currently visiting.
/// </summary>
/// <param name="condition">The condition to adapt</param>
/// <param name="type">The type of the current member (=Navigation property)</param>
/// <returns>The adapted QueryConditional</returns>
private LambdaExpression CreateExpression(QueryConditional condition, Type type)
{
var lambda = (LambdaExpression)condition.Conditional;
var conditionType = condition.Conditional.GetType().GetGenericArguments().FirstOrDefault();
// Only continue when the condition is applicable to the Type of the member
if (conditionType == null)
return null;
if (!conditionType.IsAssignableFrom(type))
return null;
var newParams = new[] { Expression.Parameter(type, "bo") };
var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement);
var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body);
lambda = Expression.Lambda(fixedBody, newParams);
return lambda;
}
}
}
QueryConditional Class:
using DomainClasses;
using System;
using System.Linq.Expressions;
namespace DataLayer
{
public class QueryConditional
{
public QueryConditional(Expression<Func<BaseClass, bool>> ex)
{
Conditional = ex;
}
public Expression<Func<BaseClass, bool>> Conditional { get; set; }
}
}
ParameterRebinder Class:
using System.Collections.Generic;
using System.Linq.Expressions;
namespace DataLayer
{
public class ParameterRebinder : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, ParameterExpression> map;
public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression node)
{
ParameterExpression replacement;
if (map.TryGetValue(node, out replacement))
node = replacement;
return base.VisitParameter(node);
}
}
}
IEntityRepository Interface:
using System;
using System.Linq;
using System.Linq.Expressions;
namespace DataLayer
{
public interface IEntityRepository<T> : IDisposable
{
IQueryable<T> All { get; }
IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties);
T Find(int id);
void InsertOrUpdate(T entity);
void Delete(int id);
void Save();
}
}
IDemoContext Interface:
using DomainClasses;
using System;
using System.Data.Entity;
namespace DataLayer
{
public interface IDemoContext : IDisposable
{
ActiveSession ActiveSession { get; }
IDbSet<Child> Children { get; }
IDbSet<Parent> Parents { get; }
int SaveChanges();
IDbSet<T> Set<T>() where T : BaseClass;
}
}