2013-05-22 5 views
7

Sto utilizzando Code First in EF. Diciamo che ho due entità:Filtraggio delle proprietà di navigazione in codice EF First

public class Farm 
{ 
    .... 
    public virtual ICollection<Fruit> Fruits {get; set;} 
} 

public class Fruit 
{ 
    ... 

} 

mio DbContext è qualcosa di simile:

public class MyDbContext : DbSet 
{ 
    .... 
    private DbSet<Farm> FarmSet{get; set;} 

    public IQueryable<Farm> Farms 
    { 
     get 
     { 
      return (from farm in FarmSet where farm.owner == myowner select farm); 
     } 
    } 
} 

mi fare questo in modo che ogni utente può vedere solo le sue aziende agricole, e non ho chiamare la Dove su ogni query al db.

Ora, voglio filtrare tutti i frutti da una fattoria, ho provato questo (in classe Farm):

from fruit in Fruits where fruit .... select fruit 

ma la query generata non include la clausola in cui, il che è molto importante perché Ho dozzine di migliaia di righe e non è efficiente caricarle tutte e filtrarle quando sono oggetti.

ho letto che pigri proprietà caricati si riempiono la prima volta che vengono raggiunti ma leggere tutti i dati, senza filtri possono essere applicati a meno che facciate qualcosa di simile:

from fruits in db.Fruits where fruit .... select fruit 

ma non posso fallo, perché Farm non ha conoscenza di DbContext (non penso che dovrebbe (?)) ma anche a me perde solo l'intero scopo di usare le proprietà di navigazione se devo lavorare con tutti i dati e non solo con quello quello appartiene alla mia fattoria.

Quindi,

  1. sto facendo qualcosa di sbagliato/fare ipotesi sbagliate?
  2. Esiste un modo per applicare un filtro a una proprietà di navigazione che viene generata nella query reale? (Sto lavorando con molti dati)

Grazie per la lettura!

risposta

8

Sfortunatamente, penso che qualsiasi approccio tu possa prendere dovrebbe coinvolgere il giochino con il contesto, non solo l'entità. Come hai visto, non puoi filtrare direttamente una proprietà di navigazione, poiché è una ICollection<T> e non una IQueryable<T>, quindi viene caricata tutto in una volta prima che tu abbia la possibilità di applicare qualsiasi filtro.

Una cosa che si potrebbe fare è quello di creare una proprietà mappata nel vostro Farm un'entità che contenga l'elenco di frutta filtrato:

public class Farm 
{ 
    .... 
    public virtual ICollection<Fruit> Fruits { get; set; } 

    [NotMapped] 
    public IList<Fruit> FilteredFruits { get; set; } 
} 

E poi, nel vostro contesto/repository, aggiungere un metodo per caricare un Farm entità e popolare FilteredFruits con i dati desiderati:

public class MyDbContext : DbContext 
{ 
    ....  

    public Farm LoadFarmById(int id) 
    { 
    Farm farm = this.Farms.Where(f => f.Id == id).Single(); // or whatever 

    farm.FilteredFruits = this.Entry(farm) 
           .Collection(f => f.Fruits) 
           .Query() 
           .Where(....) 
           .ToList(); 

    return farm; 
    } 
} 

... 

var myFarm = myContext.LoadFarmById(1234); 

Questo dovrebbe compilare myFarm.FilteredFruits con solo la raccolta filtrato, così si potrebbe usare nel modo desiderato all'interno la tua entità. Tuttavia, non ho mai provato questo approccio da solo, quindi potrebbero esserci delle insidie ​​a cui non sto pensando. Uno svantaggio principale è che funzionerebbe solo con Farm s che si carica usando quel metodo e non con le query LINQ generali che si eseguono sul set di dati MyDbContext.Farms.

Tutto ciò detto, penso che il fatto che si stia tentando di farlo potrebbe essere un segnale che si sta mettendo troppa logica aziendale nella classe di entità, quando in realtà potrebbe appartenere meglio in un livello diverso. Molto spesso, è meglio trattare le entità fondamentalmente come semplici ricettacoli per i contenuti di un record del database e lasciare tutto il filtraggio/elaborazione al repository o ovunque la logica di business/display viva. Non sono sicuro di quale tipo di applicazione stai lavorando, quindi non posso offrire alcun consiglio specifico, ma è qualcosa su cui riflettere.

Un approccio molto comune se si decide di spostare le cose fuori l'entità Farm è quello di utilizzare la proiezione:

var results = (from farm in myContext.Farms 
       where .... 
       select new { 
       Farm = farm, 
       FilteredFruits = myContext.Fruits.Where(f => f.FarmId == farm.Id && ...).ToList() 
       }).ToList(); 

... e poi utilizzare gli oggetti anonimi generati per tutto quello che si vuole fare, piuttosto che cercare per aggiungere ulteriori dati alle entità Farm.

+0

Grazie a Jeremy, ho deciso di seguire il tuo consiglio e lasciare le responsabilità di filtraggio/elaborazione nella mia classe di contesto . Ha senso perché ho bisogno solo del filtro per una delle mie entità, ma sarebbe ingombrante se ne avessi bisogno per diverse entità, non credi? Il contesto sarebbe popolato con metodi per interrogare e riempire entità. Non credo che rompa il principio di responsabilità singola, ma suona piuttosto strano, vero? – edpaez

1

Ho appena pensato di aggiungere un'altra soluzione a questo, dopo aver trascorso un po 'di tempo cercando di aggiungere i principi DDD per codificare i primi modelli. Dopo aver cercato in giro per qualche tempo ho trovato una soluzione come quella qui sotto che funziona per me.

public class FruitFarmContext : DbContext 
{ 
    public DbSet<Farm> Farms { get; set; } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     modelBuilder.Entity<Farm>().HasMany(Farm.FruitsExpression).WithMany(); 
    } 
} 

public class Farm 
{ 
    public int Id { get; set; } 
    protected virtual ICollection<Fruit> Fruits { get; set; } 
    public static Expression<Func<Farm, ICollection<Fruit>>> FruitsExpression = x => x.Fruits; 

    public IEnumerable<Fruit> FilteredFruits 
    { 
     get 
     { 
      //Apply any filter you want here on the fruits collection 
      return Fruits.Where(x => true); 
     } 
    } 
} 

public class Fruit 
{ 
    public int Id { get; set; } 

} 

L'idea è che la raccolta di frutta aziende agricole non è direttamente accessibile, ma viene invece esposto attraverso una proprietà che pre-filtri IT. Il compromesso qui è l'espressione statica che è richiesta per essere in grado di indirizzare la raccolta di frutta quando si imposta la mappatura. Ho iniziato a utilizzare questo approccio su una serie di progetti in cui voglio controllare l'accesso a raccolte di oggetti di oggetti.

+0

Ci sono implicazioni in arrivo con questa soluzione? – Oswin

2

Lazy loading non supporta il filtro; utilizzare filtered explicit loading invece:

Farm farm = dbContext.Farms.Where(farm => farm.Owner == someOwner).Single(); 

dbContext.Entry(farm).Collection(farm => farm.Fruits).Query() 
    .Where(fruit => fruit.IsRipe).Load(); 

L'approccio di caricamento esplicito richiede due viaggi di andata al database, uno per il master e uno per il dettaglio. Se è importante attenersi a una singola query, utilizzare una proiezione invece:

Farm farm = (
    from farm in dbContext.Farms 
    where farm.Owner == someOwner 
    select new { 
     Farm = farm, 
     Fruit = dbContext.Fruit.Where(fruit => fruit.IsRipe) // Causes Farm.Fruit to be eager loaded 
    }).Single().Farm; 

EF si lega sempre proprietà di navigazione alle loro entità caricati. Ciò significa che farm.Fruit conterrà la stessa raccolta filtrata della proprietà Fruit nel tipo anonimo. (Assicurati di non aver caricato nel contesto qualsiasi entità Fruit che deve essere filtrata, come descritto in Use Projections and a Repository to Fake a Filtered Eager Load.)