データベースにソフト削除を実装するフレームワークがあります(DeletedDateと呼ばれるNullable DateTime)。次のように、リポジトリを使用してメイン エンティティのリクエストを処理しています。
/// <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);
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query;
}
したがって、Children と呼ばれるナビゲーション プロパティを持つ Parent と呼ばれるエンティティによってリポジトリが使用されている場合、AllInclusive メソッドは、論理的に削除された親レコードを適切に除外しますが、論理的に削除された子レコードは引き続き含まれます。
データベースに送信されたクエリを見ると、実行する必要があるのは、SQL 結合句に " AND Children.DeletedDate IS NULL " を追加することだけで、クエリは正しい結果を返すようです。
調査中に、まさに必要なものと思われるこの投稿を見つけましたが、私の実装ではポスターと同じ結果が得られません。コードをステップ実行すると、クエリの Children 部分には何も起こらないようです。
これが私の現在の関連コードです(注:nugetからQueryInterceptorを使用):
ベースクラス:
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; }
}
}
親クラス:
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
}
}
子クラス:
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
}
}
コンテキスト クラス:
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>();
}
}
}
リポジトリ クラス:
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 クラス:
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 クラス:
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 クラス:
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 インターフェイス:
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 インターフェイス:
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;
}
}