私は、データベースにソフト削除を実装するフレームワークを持っています(DeletedDateというNullable DateTime)。私はそうのような主要なエンティティの要求を処理するためのリポジトリを使用しています:ExpressionVisitorを使用して結合でソフト削除されたレコードを除外します
/// <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;
}
リポジトリは、子供と呼ばれるナビゲーションプロパティを持つ親と呼ばれるエンティティによって使用されているのであれば、AllIncluding方法が適切ソフト削除された親レコードを除外するだろうが、ソフト削除された子レコードは引き続き含まれます。
データベースに送信されたクエリを見ると、SQL結合句 "AND Children.DeletedDate IS NULL"に追加するだけで、正しい結果が返されます。
私の研究中、this postが私の必要としているようですが、私の実装ではポスターと同じ結果が得られませんでした。コードを実行すると、クエリの子要素に何も起きないように見えます。:
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; }
}
}
親クラス:
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
}
}
子供クラス:
:ここは私の現在の関連するコード(nugetからQueryInterceptorを使用して注意してください)です
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;
}
}
私は自分のプロジェクトで同じことをする必要があります。あなたはこれに解決策を得ましたか? – Colin
まだ、私は動的に式を構築する方法を検討し始めましたが、別のプロジェクトでは取り除かれました。これを行う方法が必要なような気がするのですが、私は表現やExpressionVisitorクラスにまだ精通していません。 –
SQLサーバーのバージョンに応じて、削除されていない句を持つビューとEFが対話する方が簡単です。次に、トリガーの代わりにいくつかを追加するだけで、すべてがうまくいくはずです。 – Aron