【问题标题】:Can a DbContext enforce a filter policy?DbContext 可以强制执行过滤策略吗?
【发布时间】:2011-04-15 11:51:18
【问题描述】:

我想将一个值传递给 DbContext 的 ctor,然后让该值在相关 DbSet 上强制“过滤”。这可能......还是有更好的方法?

代码可能如下所示:

class Contact {
  int ContactId { get; set; }
  int CompanyId { get; set; }
  string Name { get; set; }
}

class ContactContext : DbContext {
  public ContactContext(int companyId) {...}
  public DbSet<Contact> Contacts { get; set; }
}

using (var cc = new ContactContext(123)) {
  // Would only return contacts where CompanyId = 123
  var all = (from i in cc.Contacts select i);

  // Would automatically set the CompanyId to 123
  var contact = new Contact { Name = "Doug" };
  cc.Contacts.Add(contact);
  cc.SaveChanges();

  // Would throw custom exception
  contact.CompanyId = 456;
  cc.SaveChanges;
}

【问题讨论】:

    标签: c# entity-framework-4.1


    【解决方案1】:

    我决定实现一个自定义 IDbSet 来处理这个问题。要使用此类,您需要传入一个 DbContext、一个过滤器表达式和(可选)一个 Action 来初始化新实体,使其符合过滤器条件。

    我已经测试了枚举集合并使用 Count 聚合函数。它们都修改了生成的 SQL,因此它们应该比在客户端过滤更有效。

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Data.Entity;
    using System.Linq;
    using System.Linq.Expressions;
    
    
    namespace MakeMyPledge.Data
    {
        class FilteredDbSet<TEntity> : IDbSet<TEntity>, IOrderedQueryable<TEntity>, IOrderedQueryable, IQueryable<TEntity>, IQueryable, IEnumerable<TEntity>, IEnumerable, IListSource
            where TEntity : class
        {
            private readonly DbSet<TEntity> Set;
            private readonly IQueryable<TEntity> FilteredSet;
            private readonly Action<TEntity> InitializeEntity;
    
            public FilteredDbSet(DbContext context)
                : this(context.Set<TEntity>(), i => true, null)
            {
            }
    
            public FilteredDbSet(DbContext context, Expression<Func<TEntity, bool>> filter)
                : this(context.Set<TEntity>(), filter, null)
            {
            }
    
            public FilteredDbSet(DbContext context, Expression<Func<TEntity, bool>> filter, Action<TEntity> initializeEntity)
                : this(context.Set<TEntity>(), filter, initializeEntity)
            {
            }
    
            private FilteredDbSet(DbSet<TEntity> set, Expression<Func<TEntity, bool>> filter, Action<TEntity> initializeEntity)
            {
                Set = set;
                FilteredSet = set.Where(filter);
                MatchesFilter = filter.Compile();
                InitializeEntity = initializeEntity;
            }
    
            public Func<TEntity, bool> MatchesFilter { get; private set; }
    
            public void ThrowIfEntityDoesNotMatchFilter(TEntity entity)
            {
                if (!MatchesFilter(entity))
                    throw new ArgumentOutOfRangeException();
            }
    
            public TEntity Add(TEntity entity)
            {
                DoInitializeEntity(entity);
                ThrowIfEntityDoesNotMatchFilter(entity);
                return Set.Add(entity);
            }
    
            public TEntity Attach(TEntity entity)
            {
                ThrowIfEntityDoesNotMatchFilter(entity);
                return Set.Attach(entity);
            }
    
            public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, TEntity
            {
                var entity = Set.Create<TDerivedEntity>();
                DoInitializeEntity(entity);
                return (TDerivedEntity)entity;
            }
    
            public TEntity Create()
            {
                var entity = Set.Create();
                DoInitializeEntity(entity);
                return entity;
            }
    
            public TEntity Find(params object[] keyValues)
            {
                var entity = Set.Find(keyValues);
                if (entity == null)
                    return null;
    
                // If the user queried an item outside the filter, then we throw an error.
                // If IDbSet had a Detach method we would use it...sadly, we have to be ok with the item being in the Set.
                ThrowIfEntityDoesNotMatchFilter(entity);
                return entity;
            }
    
            public TEntity Remove(TEntity entity)
            {
                ThrowIfEntityDoesNotMatchFilter(entity);
                return Set.Remove(entity);
            }
    
            /// <summary>
            /// Returns the items in the local cache
            /// </summary>
            /// <remarks>
            /// It is possible to add/remove entities via this property that do NOT match the filter.
            /// Use the <see cref="ThrowIfEntityDoesNotMatchFilter"/> method before adding/removing an item from this collection.
            /// </remarks>
            public ObservableCollection<TEntity> Local { get { return Set.Local; } }
    
            IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator() { return FilteredSet.GetEnumerator(); }
    
            IEnumerator IEnumerable.GetEnumerator() { return FilteredSet.GetEnumerator(); }
    
            Type IQueryable.ElementType { get { return typeof(TEntity); } }
    
            Expression IQueryable.Expression { get { return FilteredSet.Expression; } }
    
            IQueryProvider IQueryable.Provider { get { return FilteredSet.Provider; } }
    
            bool IListSource.ContainsListCollection { get { return false; } }
    
            IList IListSource.GetList() { throw new InvalidOperationException(); }
    
            void DoInitializeEntity(TEntity entity)
            {
                if (InitializeEntity != null)
                    InitializeEntity(entity);
            }
        }
    }
    

    【讨论】:

    • 这太棒了!有什么办法可以过滤延迟加载的项目
    • 这个例子效果很好,但我发现了一个给我带来问题的案例......包含不适用于这样的数据集。我尝试为此(public static IQueryable&lt;TEntity&gt; Include&lt;TEntity, TProperty&gt;(this IDbSet&lt;TEntity&gt; dbSet, Expression&lt;Func&lt;TEntity, TProperty&gt;&gt; expression))创建一个扩展方法,在其中检查 dbSet 的类型。如果是我的类型,我在原始DbSet 上调用Include 方法,如果不是,我在IQueryable&lt;TEntity&gt; 上调用它。问题是Include 必须在IDbSet 上调用,而不是在其他东西上(例如AsNoTracking 的结果)......有什么想法吗?
    • 这行得通!但是 FilteredSet = set.Where(filter) 行为与原始 dbset 不同,是否考虑使用属性?
    • 这个答案对我很有帮助,谢谢。只有一个问题,您会发现 Includes 不再起作用 ctx.WrappedDbSet.Include(x =&gt; x.someProperty)。但是添加支持很容易——只需添加一个方法public IDbSet&lt;Document&gt; Include(string path) {} 并在FilteredSet 上调用Include。
    【解决方案2】:

    EF 没有任何“过滤”功能。您可以尝试通过继承自定义 DbSet 来实现类似的目标,但我认为这仍然会有问题。例如 DbSet 直接实现 IQueryable 所以可能没有办法包含自定义条件。

    这将需要一些包装器来处理这些要求(可以是存储库):

    • select 中的条件可以通过在 DbSet 周围包装方法来处理,该方法将添加 Where 条件
    • 插入也可以通过包装方法处理
    • 必须通过覆盖SaveChanges 并使用context.ChangeTracker 来获取所有更新的实体来处理更新。然后您可以检查实体是否被修改。

    我所说的包装器并不是指自定义DbSet 实现——这太复杂了:

    public class MyDal
    {
        private DbSet<MyEntity> _set;
    
        public MyDal(DbContext context)
        {
            _set = context.Set<MyEntity>();
        }
    
        public IQueryable<MyEntity> GetQuery()
        {
            return _set.Where(e => ...);
        }
    
        // Attach, Insert, Delete
    }
    

    【讨论】:

    • 按照 Ladislav 所说,查看规范模式:devlicio.us/blogs/jeff_perrin/archive/2006/12/13/… 无需深入了解大量细节,它可能有助于整合您的过滤器。
    • @DDiVita 不确定该帖子对我有多大帮助。我想在 EF 生成的 SQL WHERE 子句中注入一个额外的过滤器。
    • @Ladislav - 我试图创建一个新的 IDbSet 类,它基本上包装了 DbContext 提供的 DbSet。我按如下方式覆盖了 GetEnumerator: ` public IEnumerator GetEnumerator() { return (from i in mDbSet where i.AccountNumber.Value == mAccountNumber.Value select i).GetEnumerator(); }` 这适用于枚举集合的查询......但不适用于像 MySet.Count() 这样的聚合查询。有什么想法吗?
    • @Doug:你把它想得太复杂了。我将在我的答案中添加一些示例。
    • @Ladislav - 我同意;真的没有理由做一个完整的 IDbSet 实现。我发现重写 Expression 属性非常困难。
    猜你喜欢
    • 1970-01-01
    • 2014-07-13
    • 1970-01-01
    • 1970-01-01
    • 2022-08-02
    • 2021-04-16
    • 2014-06-26
    • 2017-05-17
    • 1970-01-01
    相关资源
    最近更新 更多