2011-04-15 25 views
30

Me gustaría pasar un valor al ctor de un DbContext y luego hacer que ese valor imponga el "filtrado" en los DbSets relacionados. ¿Es esto posible ... o hay un mejor enfoque?¿Puede un DbContext aplicar una política de filtro?

Código podría tener este aspecto:

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

Respuesta

45

I decidió aplicar una costumbre IDbSet para hacer frente a esto. Para usar esta clase, transfiere un DbContext, una expresión de filtro y (opcionalmente) una Acción para inicializar nuevas entidades para que cumplan con los criterios de filtro.

He probado la enumeración del conjunto y el uso de las funciones de agregado de conteo. Ambos modifican el SQL que se genera, por lo que deberían ser mucho más eficientes que el filtrado en el cliente.

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); 
     } 
    } 
} 
+3

¡Esto es genial! ¿Hay alguna manera de hacer que esto filtre los elementos cargados perezosos? – maxfridbe

+1

Este ejemplo funciona muy bien, pero encontré un caso que me causa problemas ... Incluye no se aplican en un conjunto de datos de este tipo.Intenté crear un método de extensión para esto ('public static IQueryable Include (esta expresión IDbSet dbSet, Expression >)) donde verifico el tipo de dbSet. Si es mi tipo, llamo al método 'Include' en el' DbSet' Original, si no lo es, lo llamo 'IQueryable '. El problema es que 'Include' debe invocarse en' IDbSet', no en otra cosa (por ejemplo, el resultado de 'AsNoTracking') ... ¿Alguna idea? – ghigad

+0

Esto funciona! Sin embargo, el comportamiento de FilteredSet = set.Where (filter) es diferente del archivo dbset original, ¿considera usar una propiedad? – Yiping

5

EF no tiene ninguna función de "filtro". Puede tratar de lograr algo así heredando el DbSet personalizado, pero creo que seguirá siendo problemático. Por ejemplo, DbSet implementa directamente IQueryable, por lo que probablemente no haya forma de cómo incluir una condición personalizada.

Esto requerirá un poco de envoltura que se encargará de estos requisitos (puede ser repositorio):

  • Condición de selección puede ser manejado por envolver método alrededor DbSet que se sumarán Where condición
  • Insertar puede ser manejado por el método de envoltura también
  • La actualización se debe manejar anulando SaveChanges y usando context.ChangeTracker para obtener todas las entidades actualizadas. Luego puede verificar si las entidades fueron modificadas.

Por envoltorio no me refiero a medida DbSet aplicación - que es demasiado complejo:

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 
} 
+0

Junto con lo que dijo Ladislav, revisar el modelo de pliego de condiciones: http://devlicio.us/blogs/jeff_perrin/archive/2006/12/13/the-specification-pattern.aspx Sin entrar en una tonelada de detalles, puede ayudar con la consolidación de sus filtros. – DDiVita

+1

@DDiVita No estoy seguro de que la publicación me ayude mucho. Quiero inyectar un filtro adicional en la cláusula SQL WHERE que genera EF. –

+0

@Ladislav - He intentado crear una nueva clase IDbSet <> que básicamente envuelve el DbSet <> proporcionado por DbContext. Sobreescribí el GetEnumerator de la siguiente manera: 'public IEnumerator GetEnumerator() { return (de i en mDbSet donde i.AccountNumber.Value == mAccountNumber.Value seleccione i) .GetEnumerator(); } ' Esto funcionó para las consultas que enumeran el conjunto ... pero no para las consultas que se agregan como MySet.Count(). ¿Alguna idea? –

Cuestiones relacionadas