c# – 使用ExpressionVisitor排除连接中的软删除记录

前端之家收集整理的这篇文章主要介绍了c# – 使用ExpressionVisitor排除连接中的软删除记录前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我有一个框架,在数据库中实现软删除(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()有问题.如果我找到一些时间,那么我会尝试一下并回复你.

猜你在找的C#相关文章