我有一个框架,在数据库中实现软删除(Nullable DateTime称为DeletedDate).我正在使用Repository来处理主要实体请求,如下所示:
/// <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; }
因此,如果存储库由名为Parent的实体使用,该实体具有名为Children的导航属性,则AllIncluding方法将正确过滤掉软删除的父记录,但仍会包含软删除的子记录.
查看发送到数据库的查询,似乎所有需要做的就是添加到sql join子句“AND Children.DeletedDate IS NULL”,查询将返回正确的结果.
在我的研究过程中,我发现this post似乎正是我所需要的,但是我的实现并没有得到海报的相同结果.单步执行代码,查询的Children部分似乎没有任何结果.
这是我当前的相关代码(注意:使用nuget中的QueryInterceptor):
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,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,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; } }
解决方法
问题是您想要在AllIncluding方法中使用Include()语句添加条件. queryinterceptor包不支持Include()方法.只有使其工作的解决方案才是使用Include语句.
当您执行以下操作时,一切正常:
Articles.Select(x => new { Vat = x.VatTypes }) .InterceptWith(Visitor);
因此,当上述内容转换为sql时,您将看到Where VatTypes.IsDeleted = 0被添加到查询中.
是否真的有必要使用includeAll方法,从性能角度看,这对我来说是一个巨大的开销,因为你从数据库加载了所有内容.
编辑:再次阅读一些较旧的帖子后,看起来实际上可以使用InterludeWith方法和Include()语句.也许正是ExpressionVisitor与Include()有问题.如果我找到一些时间,那么我会尝试一下并回复你.