在将所有导航属性加载(懒惰或渴望)到内存之前对其进行过滤

对于未来的访问者:对于EF6,您最好使用filter,例如通过此项目: https : //github.com/jbogard/EntityFramework.Filters

在我们正在构建的应用程序中,我们应用“软删除”模式,其中每个类都有一个’已删除’布尔值。 实际上,每个类都只是inheritance自这个基类:

public abstract class Entity { public virtual int Id { get; set; } public virtual bool Deleted { get; set; } } 

举一个简短的例子,假设我有GymMemberWorkout类:

 public class GymMember: Entity { public string Name { get; set; } public virtual ICollection Workouts { get; set; } } public class Workout: Entity { public virtual DateTime Date { get; set; } } 

当我从数据库中获取健身房成员列表时,我可以确保没有获取任何“已删除”健身房成员,如下所示:

 var gymMembers = context.GymMembers.Where(g => !g.Deleted); 

但是,当我遍历这些健身房成员时,他们的Workouts从数据库加载而不考虑他们的Deleted标志。 虽然我不能责怪entity framework没有拿到这个,我想以某种方式配置或拦截延迟属性加载,以便永远不会加载已删除的导航属性。

我一直在考虑我的选择,但它们看起来很稀缺:

  • 转到Database First并为每个一对多属性的每个对象使用条件映射 。

这根本不是一种选择,因为手动工作太多了。 (我们的应用程序非常庞大,每天都变得越来越大)。 我们也不想放弃使用Code First的优势(其中有很多)

  • 始终急切地加载导航属性 。

再次,不是一个选择。 此配置仅适用于每个实体。 总是急切地加载实体也会造成严重的性能损失。

  • 应用表达式访问者模式,它会在找到IQueryable任何地方自动注入.Where(e => !e.Deleted) ,如此处和此处所述 。

我实际上在概念validation应用程序中对此进行了测试,并且它运行得非常好。 这是一个非常有趣的选项,但是,它无法对延迟加载的导航属性应用过滤。 这很明显,因为这些惰性属性不会出现在表达式/查询中,因此无法替换。 我想知道entity framework是否允许在其DynamicProxy类中的某个位置加载延迟属性的注入点。 我也担心会产生其他后果,例如可能会破坏EF中的Include机制。

  • 编写实现ICollection但自动过滤Deleted实体的自定义类。

这实际上是我的第一个方法。 我们的想法是为内部使用自定义Collection类的每个集合属性使用backing属性:

 public class GymMember: Entity { public string Name { get; set; } private ICollection _workouts; public virtual ICollection Workouts { get { return _workouts ?? (_workouts = new CustomCollection()); } set { _workouts = new CustomCollection(value); } } } 

虽然这种方法实际上并不坏,但我仍然遇到一些问题:

  • 它仍会将所有Workout加载到内存中,并在命中属性设置器时过滤DeletedWorkout 。 以我的拙见,这太迟了。

  • 执行的查询与加载的数据之间存在逻辑不匹配。

想象一下我想要一个自上周以来做过锻炼的健身会员名单的情景:

 var gymMembers = context.GymMembers.Where(g => g.Workouts.Any(w => w.Date >= DateTime.Now.AddDays(-7).Date)); 

此查询可能会返回一个健身房成员,该成员只有已删除但仍满足谓词的锻炼。 一旦将它们加载到内存中,就好像这个健身房成员根本没有锻炼! 您可以说开发人员应该知道Deleted并始终将其包含在他的查询中,但这是我真正想要避免的。 也许ExpressionVisitor可以再次提供答案。

  • 使用CustomCollection时,实际上不可能将导航属性标记为Deleted

想象一下这种情况:

 var gymMember = context.GymMembers.First(); gymMember.Workouts.First().Deleted = true; context.SaveChanges();` 

您可能希望在数据库中更新相应的Workout记录,这样你就错了! 由于任何变化都会由ChangeTracker检查ChangeTracker ,因此属性gymMember.Workouts将突然减少1次锻炼。 那是因为CustomCollection会自动过滤已删除的实例,还记得吗? 所以现在Entity Framework认为需要删除锻炼,EF会尝试将FK设置为​​null,或者实际删除记录。 (取决于数据库的配置方式)。 这是我们试图避免使用软删除模式开始!

我偶然发现了一篇有趣的博客文章,它覆盖了DbContext的默认SaveChanges方法,因此任何带有EntityState.Deleted条目EntityState.Deleted更改回EntityState.Modified但这EntityState.Modified感觉“hacky”而且不安全。 但是,如果它解决了没有任何意外副作用的问题,我愿意尝试一下。


所以我在这里是StackOverflow。 我已经对我的选择进行了相当广泛的研究,如果我自己可以这么说的话,我就是在我的智慧结束。 所以现在我转向你。 您是如何在企业应用程序中实现软删除的?

重申一下,这些是我正在寻找的要求:

  • 查询应自动排除数据库级别上的Deleted实体
  • 删除实体并调用“SaveChanges”应该只是更新相应的记录而没有其他副作用。
  • 加载导航属性时,无论是懒惰还是急切,都应自动排除Deleted的属性。

我期待着任何和所有建议,谢谢你提前。

经过大量研究,我终于找到了实现我想要的方法。 它的要点是我在对象上下文中使用事件处理程序拦截物化实体,然后在我可以找到的每个集合属性中注入我的自定义集合类(使用reflection)。

最重要的部分是拦截“DbCollectionEntry”,负责加载相关集合属性的类。 通过在实体和DbCollectionEntry之间摆动自己,我可以完全控制何时以及如何加载。 唯一的缺点是这个DbCollectionEntry类几乎没有公共成员,这要求我使用reflection来操纵它。

这是我的自定义集合类,它实现ICollection并包含对相应DbCollectionEntry的引用:

 public class FilteredCollection  : ICollection where TEntity : Entity { private readonly DbCollectionEntry _dbCollectionEntry; private readonly Func _compiledFilter; private readonly Expression> _filter; private ICollection _collection; private int? _cachedCount; public FilteredCollection(ICollection collection, DbCollectionEntry dbCollectionEntry) { _filter = entity => !entity.Deleted; _dbCollectionEntry = dbCollectionEntry; _compiledFilter = _filter.Compile(); _collection = collection != null ? collection.Where(_compiledFilter).ToList() : null; } private ICollection Entities { get { if (_dbCollectionEntry.IsLoaded == false && _collection == null) { IQueryable query = _dbCollectionEntry.Query().Cast().Where(_filter); _dbCollectionEntry.CurrentValue = this; _collection = query.ToList(); object internalCollectionEntry = _dbCollectionEntry.GetType() .GetField("_internalCollectionEntry", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(_dbCollectionEntry); object relatedEnd = internalCollectionEntry.GetType() .BaseType.GetField("_relatedEnd", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(internalCollectionEntry); relatedEnd.GetType() .GetField("_isLoaded", BindingFlags.NonPublic | BindingFlags.Instance) .SetValue(relatedEnd, true); } return _collection; } } #region ICollection Members void ICollection.Add(TEntity item) { if(_compiledFilter(item)) Entities.Add(item); } void ICollection.Clear() { Entities.Clear(); } Boolean ICollection.Contains(TEntity item) { return Entities.Contains(item); } void ICollection.CopyTo(TEntity[] array, Int32 arrayIndex) { Entities.CopyTo(array, arrayIndex); } Int32 ICollection.Count { get { if (_dbCollectionEntry.IsLoaded) return _collection.Count; return _dbCollectionEntry.Query().Cast().Count(_filter); } } Boolean ICollection.IsReadOnly { get { return Entities.IsReadOnly; } } Boolean ICollection.Remove(TEntity item) { return Entities.Remove(item); } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return Entities.GetEnumerator(); } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return ( ( this as IEnumerable ).GetEnumerator() ); } #endregion } 

如果你浏览它,你会发现最重要的部分是“实体”属性,它会延迟加载实际值。 在FilteredCollection的构造函数中,我传递了一个可选的ICollection,用于已经急切加载集合的场景。

当然,我们仍然需要配置entity framework,以便在有集合属性的任何地方使用我们的FilteredCollection。 这可以通过挂钩到Entity Framework的底层ObjectContext的ObjectMaterialized事件来实现:

 (this as IObjectContextAdapter).ObjectContext.ObjectMaterialized += delegate(Object sender, ObjectMaterializedEventArgs e) { if (e.Entity is Entity) { var entityType = e.Entity.GetType(); IEnumerable collectionProperties; if (!CollectionPropertiesPerType.TryGetValue(entityType, out collectionProperties)) { CollectionPropertiesPerType[entityType] = (collectionProperties = entityType.GetProperties() .Where(p => p.PropertyType.IsGenericType && typeof(ICollection<>) == p.PropertyType.GetGenericTypeDefinition())); } foreach (var collectionProperty in collectionProperties) { var collectionType = typeof(FilteredCollection<>).MakeGenericType(collectionProperty.PropertyType.GetGenericArguments()); DbCollectionEntry dbCollectionEntry = Entry(e.Entity).Collection(collectionProperty.Name); dbCollectionEntry.CurrentValue = Activator.CreateInstance(collectionType, new[] { dbCollectionEntry.CurrentValue, dbCollectionEntry }); } } }; 

这看起来相当复杂,但它本质上是扫描物化类型的集合属性并将值更改为过滤集合。 它还将DbCollectionEntry传递给过滤后的集合,以便它可以发挥其魔力。

这涵盖了整个“装载实体”部分。 到目前为止唯一的缺点是急切加载的集合属性仍将包含已删除的实体,但它们会在FilterCollection类的“Add”方法中被过滤掉。 这是一个可以接受的缺点,虽然我还没有对它如何影响SaveChanges()方法进行一些测试。

当然,这仍然存在一个问题:查询没有自动过滤。 如果您想要获取过去一周进行锻炼的健身会员,您希望自动排除已删除的锻炼。

这是通过ExpressionVisitor实现的,它自动将’.Where(e =>!e.Deleted)’filter应用于它在给定表达式中可以找到的每个IQueryable。

这是代码:

 public class DeletedFilterInterceptor: ExpressionVisitor { public Expression> Filter { get; set; } public DeletedFilterInterceptor() { Filter = entity => !entity.Deleted; } protected override Expression VisitMember(MemberExpression ex) { return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(Filter, ex) ?? base.VisitMember(ex); } private Expression CreateWhereExpression(Expression> filter, Expression ex) { var type = ex.Type;//.GetGenericArguments().First(); var test = CreateExpression(filter, 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); } private LambdaExpression CreateExpression(Expression> condition, Type type) { var lambda = (LambdaExpression) condition; if (!typeof(Entity).IsAssignableFrom(type)) return null; var newParams = new[] { Expression.Parameter(type, "entity") }; 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; } } public class ParameterRebinder : ExpressionVisitor { private readonly Dictionary _map; public ParameterRebinder(Dictionary map) { _map = map ?? new Dictionary(); } public static Expression ReplaceParameters(Dictionary 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); } } 

我的运行时间有点短暂,所以我稍后会回到这篇文章的更多细节,但是它的要点是写下来的,对于那些渴望尝试一切的人来说; 我在这里发布了完整的测试应用程序: https : //github.com/amoerie/TestingGround

但是,可能仍然存在一些错误,因为这是一项非常重要的工作。 虽然概念性的想法是合理的,但是一旦我整齐地重构了所有内容并且找到时间为此编写一些测试,我希望它能够很快完成。

一种可能的方式可能是使用具有基本规范的规范,该规范检查所有查询的软删除标志以及包含策略。

我将说明我在项目中使用的规范模式的调整版本(源自此博客文章 )

 public abstract class SpecificationBase : ISpecification where T : Entity { private readonly IPredicateBuilderFactory _builderFactory; private IPredicateBuilder _predicateBuilder; protected SpecificationBase(IPredicateBuilderFactory builderFactory) { _builderFactory = builderFactory; } public IPredicateBuilder PredicateBuilder { get { return _predicateBuilder ?? (_predicateBuilder = BuildPredicate()); } } protected abstract void AddSatisfactionCriterion(IPredicateBuilder predicateBuilder); private IPredicateBuilder BuildPredicate() { var predicateBuilder = _builderFactory.Make(); predicateBuilder.Check(candidate => !candidate.IsDeleted) AddSatisfactionCriterion(predicateBuilder); return predicateBuilder; } } 

IPredicateBuilder是LINQKit.dll中包含的谓词构建器的包装器。

规范基类负责创建谓词构建器。 创建后,可以添加应该应用于所有查询的条件。 然后可以将谓词构建器传递给inheritance的规范以添加更多条件。 例如:

 public class IdSpecification : SpecificationBase where T : Entity { private readonly int _id; public IdSpecification(int id, IPredicateBuilderFactory builderFactory) : base(builderFactory) { _id = id; } protected override void AddSatisfactionCriterion(IPredicateBuilder predicateBuilder) { predicateBuilder.And(entity => entity.Id == _id); } } 

IdSpecification的完整谓词将是:

 entity => !entity.IsDeleted && entity.Id == _id 

然后可以将规范传递给使用PredicateBuilder属性构建where子句的存储库:

  public IQueryable FindAll(ISpecification spec) { return context.AsExpandable().Where(spec.PredicateBuilder.Complete()).AsQueryable(); } 

AsExpandable()是LINQKit.dll的一部分。

关于包含/延迟加载属性,可以使用关于包含的进一步属性来扩展规范。 规范库可以添加基础包含然后子规范添加其包含。 然后,存储库可以在从db中获取之前应用规范中的包含。

  public IQueryable Apply(IDbSet context, ISpecification specification) { if (specification.IncludePaths == null) return context; return specification.IncludePaths.Aggregate>(context, (current, path) => current.Include(path)); } 

如果有什么不清楚,请告诉我。 我试图不把它变成一个怪物post,所以可能会遗漏一些细节。

编辑:我意识到我没有完全回答你的问题; 导航属性。 如果您将导航属性设置为内部(使用此post配置它并创建IQueryable的非映射公共属性,该怎么办?非映射属性可以具有自定义属性,并且存储库将基本规范的谓词添加到where,而不是急切地加载当有人确实应用了一个热切的操作时,filter将适用。例如:

  public T Find(int id) { var entity = Context.SingleOrDefault(x => x.Id == id); if (entity != null) { foreach(var property in entity.GetType() .GetProperties() .Where(info => info.CustomAttributes.OfType().Any())) { var collection = (property.GetValue(property) as IQueryable); collection = collection.Where(spec.PredicateBuilder.Complete()); } } return entity; } 

我没有测试上面的代码,但它可以与一些调整:)

编辑2:删除。

如果您使用的是通用/通用存储库,则只需在delete方法中添加一些其他function:

  public void Delete(T entity) { var castedEntity = entity as Entity; if (castedEntity != null) { castedEntity.IsDeleted = true; } else { _context.Remove(entity); } } 

您是否考虑过使用数据库中的视图加载已排除已删除项目的问题实体?

它确实意味着您将需要使用存储过程来映射INSERT / UPDATE / DELETEfunction,但如果Workout映射到省略删除行的View,它肯定会解决您的问题。 此外 – 在代码第一种方法中,这可能无法正常工作……