DbContext可以强制执行过滤策略吗?

时间:2011-04-15 11:51:19

标签: c# entity-framework-4.1

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

代码可能如下所示:

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;
}

2 个答案:

答案 0 :(得分:47)

我决定实现一个自定义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);
        }
    }
}

答案 1 :(得分:5)

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
}