Может ли DbContext применять политику фильтрации? - PullRequest
31 голосов
/ 15 апреля 2011

Я хотел бы передать значение в ctor DbContext, а затем заставить это значение принудительно «фильтровать» в связанных 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 ]

47 голосов
/ 21 апреля 2011

Я решил реализовать собственный 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);
        }
    }
}
5 голосов
/ 15 апреля 2011

EF не имеет функции фильтра. Вы можете попытаться добиться чего-то подобного, наследуя custom 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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...