gpt4 book ai didi

c# - 在将所有导航属性加载(延迟或急切)到内存之前过滤它们

转载 作者:IT王子 更新时间:2023-10-29 04:19:41 26 4
gpt4 key购买 nike

对于 future 的访问者:对于 EF6,您可能最好使用过滤器,例如通过这个项目:https://github.com/jbogard/EntityFramework.Filters

在我们正在构建的应用程序中,我们应用“软删除”模式,其中每个类都有一个“已删除” bool 值。实际上,每个类都简单地从这个基类继承:

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<Workout> Workouts { get; set; }
}

public class Workout: Entity
{
public virtual DateTime Date { get; set; }
}

当我从数据库中获取健身房成员(member)列表时,我可以确保没有获取任何“已删除”的健身房成员(member),如下所示:
var gymMembers = context.GymMembers.Where(g => !g.Deleted);

然而,当我遍历这些健身房成员时,他们的 Workouts从数据库加载而不考虑它们的 Deleted旗帜。虽然我不能责怪 Entity Framework 没有注意到这一点,但我想以某种方式配置或拦截延迟属性加载,以便永远不会加载已删除的导航属性。

我一直在考虑我的选择,但它们似乎很少:
  • Going to Database First and use conditional mapping for every object for every one-to-many property .

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

  • 再次,不是一个选项。此配置仅适用于每个实体。总是急切地加载实体也会造成严重的性能损失。
  • 应用自动注入(inject)的表达式访问者模式 .Where(e => !e.Deleted)在任何地方找到 IQueryable<Entity> ,如描述 herehere .

  • 我实际上在概念验证应用程序中对此进行了测试,并且效果很好。
    这是一个非常有趣的选项,但遗憾的是,它无法对延迟加载的导航属性应用过滤。这很明显,因为那些惰性属性不会出现在表达式/查询中,因此无法替换。我想知道 Entity Framework 是否允许在其 DynamicProxy 中的某处设置注入(inject)点加载惰性属性的类。
    我也担心其他后果,例如破坏 Include的可能性。 EF 中的机制。
  • 编写一个实现 ICollection 但过滤 Deleted 的自定义类实体自动。

  • 这实际上是我的第一个方法。这个想法是为每个内部使用自定义 Collection 类的集合属性使用一个支持属性:
    public class GymMember: Entity
    {
    public string Name { get; set; }

    private ICollection<Workout> _workouts;
    public virtual ICollection<Workout> Workouts
    {
    get { return _workouts ?? (_workouts = new CustomCollection()); }
    set { _workouts = new CustomCollection(value); }
    }

    }

    虽然这种方法实际上还不错,但我仍然有一些问题:
  • 它仍然加载所有 Workout s 进入内存并过滤 Deleted当属性 setter 被命中时。以我的拙见,这为时已晚。
  • 执行的查询和加载的数据之间存在逻辑不匹配。

  • 想象一个场景,我想要一个自上周以来进行锻炼的健身房成员的列表:
    var gymMembers = context.GymMembers.Where(g => g.Workouts.Any(w => w.Date >= DateTime.Now.AddDays(-7).Date));

    此查询可能会返回一个健身房成员,该成员仅具有已删除但也满足谓词的锻炼。一旦它们被加载到内存中,就好像这个健身房成员根本没有锻炼一样!
    你可以说开发者应该知道 Deleted并始终将其包含在他的查询中,但这是我真正想避免的。也许 ExpressionVisitor 可以在这里再次提供答案。
  • 实际上不可能将导航属性标记为 Deleted使用 CustomCollection 时。

  • 想象一下这个场景:
    var gymMember = context.GymMembers.First();
    gymMember.Workouts.First().Deleted = true;
    context.SaveChanges();`

    您会期望适当的 Workout记录在数据库中更新,你就错了!自 gymMember正在接受 ChangeTracker 的检查如有任何更改,属性(property) gymMember.Workouts会突然减少 1 次锻炼。那是因为 CustomCollection 会自动过滤已删除的实例,还记得吗?所以现在 Entity Framework 认为该锻炼需要删除,EF 会尝试将 FK 设置为 null,或者实际删除该记录。 (取决于您的数据库的配置方式)。这就是我们一开始就试图通过软删除模式避免的!!!

    我偶然发现了一个 interesting blog覆盖默认值的帖子 SaveChanges DbContext的方法以便任何带有 EntityState.Deleted 的条目改回 EntityState.Modified但这又让人感觉“hacky”而且相当不安全。但是,如果它可以解决问题而没有任何意外的副作用,我愿意尝试一下。

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

    重申一下,这些是我正在寻找的要求:
  • 查询应自动排除 Deleted数据库级别的实体
  • 删除一个实体并调用“SaveChanges”应该只是更新相应的记录并且没有其他副作用。
  • 当加载导航属性时,无论是懒惰的还是急切的,Deleted应该自动排除。

  • 我期待着任何和所有建议,在此先感谢您。

    最佳答案

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

    最重要的部分是拦截“DbCollectionEntry”,该类负责加载相关的集合属性。通过在实体和 DbCollectionEntry 之间摆动,我可以完全控制加载的时间和方式。唯一的缺点是这个 DbCollectionEntry 类几乎没有公共(public)成员,这需要我使用反射来操作它。

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

    public class FilteredCollection <TEntity> : ICollection<TEntity> where TEntity : Entity
    {
    private readonly DbCollectionEntry _dbCollectionEntry;
    private readonly Func<TEntity, Boolean> _compiledFilter;
    private readonly Expression<Func<TEntity, Boolean>> _filter;
    private ICollection<TEntity> _collection;
    private int? _cachedCount;

    public FilteredCollection(ICollection<TEntity> collection, DbCollectionEntry dbCollectionEntry)
    {
    _filter = entity => !entity.Deleted;
    _dbCollectionEntry = dbCollectionEntry;
    _compiledFilter = _filter.Compile();
    _collection = collection != null ? collection.Where(_compiledFilter).ToList() : null;
    }

    private ICollection<TEntity> Entities
    {
    get
    {
    if (_dbCollectionEntry.IsLoaded == false && _collection == null)
    {
    IQueryable<TEntity> query = _dbCollectionEntry.Query().Cast<TEntity>().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<T> Members

    void ICollection<TEntity>.Add(TEntity item)
    {
    if(_compiledFilter(item))
    Entities.Add(item);
    }

    void ICollection<TEntity>.Clear()
    {
    Entities.Clear();
    }

    Boolean ICollection<TEntity>.Contains(TEntity item)
    {
    return Entities.Contains(item);
    }

    void ICollection<TEntity>.CopyTo(TEntity[] array, Int32 arrayIndex)
    {
    Entities.CopyTo(array, arrayIndex);
    }

    Int32 ICollection<TEntity>.Count
    {
    get
    {
    if (_dbCollectionEntry.IsLoaded)
    return _collection.Count;
    return _dbCollectionEntry.Query().Cast<TEntity>().Count(_filter);
    }
    }

    Boolean ICollection<TEntity>.IsReadOnly
    {
    get
    {
    return Entities.IsReadOnly;
    }
    }

    Boolean ICollection<TEntity>.Remove(TEntity item)
    {
    return Entities.Remove(item);
    }

    #endregion

    #region IEnumerable<T> Members

    IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
    {
    return Entities.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
    return ( ( this as IEnumerable<TEntity> ).GetEnumerator() );
    }

    #endregion
    }

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

    当然,我们仍然需要配置 Entity Framework,以便我们的 FilteredCollection 在任何有集合属性的地方使用。这可以通过 Hook 到 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<PropertyInfo> 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 类的“添加”方法中被过滤掉了。这是一个可以接受的缺点,尽管我还没有对这如何影响 SaveChanges() 方法进行一些测试。

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

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

    这是代码:
    public class DeletedFilterInterceptor: ExpressionVisitor
    {
    public Expression<Func<Entity, bool>> 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<Func<Entity, bool>> 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<Func<Entity, bool>> 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<ParameterExpression, ParameterExpression> _map;

    public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
    {
    _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);
    }
    }

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

    但是,可能仍然存在一些错误,因为这是一项正在进行的工作。不过,这个概念是合理的,我希望一旦我巧妙地重构了所有内容并有时间为此编写一些测试,它就会很快完全发挥作用。

    关于c# - 在将所有导航属性加载(延迟或急切)到内存之前过滤它们,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18623928/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com