Может ли DbContext применять политику фильтрации?

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

person Doug Clutter    schedule 15.04.2011    source источник


Ответы (2)


Я решил реализовать собственный IDbSet, чтобы справиться с этим. Чтобы использовать этот класс, вы передаете DbContext, выражение фильтра и (необязательно) действие для инициализации новых сущностей, чтобы они соответствовали критериям фильтра.

Я протестировал перечисление набора и использование агрегатных функций 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);
        }
    }
}
person Doug Clutter    schedule 21.04.2011
comment
Отлично! Есть ли способ заставить это фильтровать лениво загруженные элементы - person maxfridbe; 08.03.2012
comment
Этот пример отлично работает, но я обнаружил случай, который вызывает у меня проблемы... Включения не применяются к такому набору данных. Я попытался создать метод расширения для этого (public static IQueryable<TEntity> Include<TEntity, TProperty>(this IDbSet<TEntity> dbSet, Expression<Func<TEntity, TProperty>> expression)), где я проверяю тип dbSet. Если это мой тип, я вызываю метод Include для исходного DbSet, если нет, я вызываю его для IQueryable<TEntity>. Проблема в том, что Include нужно вызывать для IDbSet, а не для чего-то еще (например, результат AsNoTracking)... Есть идеи? - person ghigad; 26.01.2015
comment
Это работает! Однако поведение FilteredSet = set.Where(filter) отличается от исходного набора баз данных, рассмотрите возможность использования свойства? - person Yiping; 17.03.2017
comment
Этот ответ был очень полезен для меня, спасибо. Есть только одна проблема: вы обнаружите, что Включения больше не работают ctx.WrappedDbSet.Include(x => x.someProperty). Но добавить поддержку легко — просто добавьте метод public IDbSet<Document> Include(string path) {} и вызовите Include для FilteredSet. - person ; 14.12.2018

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
}
person Ladislav Mrnka    schedule 15.04.2011
comment
Наряду с тем, что сказал Ладислав, ознакомьтесь с шаблоном спецификации: devlicio.us/blogs/jeff_perrin/archive/2006/12/13/ Не вдаваясь в подробности, это может помочь в объединении ваших фильтров. - person DDiVita; 16.04.2011
comment
@DDiVita Не уверен, что этот пост мне сильно поможет. Я хочу ввести дополнительный фильтр в предложение SQL WHERE, которое генерирует EF. - person Doug Clutter; 20.04.2011
comment
@Ladislav - я попытался создать новый класс IDbSet‹›, который в основном обертывает DbSet‹›, предоставляемый DbContext. Я переопределил GetEnumerator следующим образом: `public IEnumerator‹T› GetEnumerator() { return (from i в mDbSet, где i.AccountNumber.Value == mAccountNumber.Value select i).GetEnumerator(); }` Это работало для запросов, которые перечисляют набор... но не для запросов, которые объединяются, например, MySet.Count(). Есть предположения? - person Doug Clutter; 20.04.2011
comment
@Doug: Вы восприняли это слишком сложно. Я добавлю несколько примеров в свой ответ. - person Ladislav Mrnka; 20.04.2011
comment
@Ладислав - согласен; на самом деле нет причин делать полную реализацию IDbSet. Я обнаружил, что переопределение свойства Expression чрезвычайно сложно. - person Doug Clutter; 21.04.2011
comment
@Ladislav - Хотя полная реализация IDbSet может быть излишней, я реализовал общую версию, которую можно использовать где угодно. Спасибо, что помогли поставить меня на правильный путь. - person Doug Clutter; 21.04.2011