9

La mia applicazione utilizza Entity Framework 7 e il pattern di repository.Uso del pattern di repository per caricare entità entit utilizzando ThenIclude

Il metodo GetById sul repository supporta eager loading di entità figlio:

public virtual TEntity GetById(int id, params Expression<Func<TEntity, object>>[] paths) 
    { 
     var result = this.Set.Include(paths.First()); 
     foreach (var path in paths.Skip(1)) 
     { 
      result = result.Include(path); 
     } 
     return result.FirstOrDefault(e => e.Id == id); 
    } 

L'uso è come segue per recuperare un prodotto (il cui ID è 2) insieme con gli ordini e le parti associate a tale prodotto:

productRepository.GetById(2, p => p.Orders, p => p.Parts); 

Desidero migliorare questo metodo per supportare il caricamento impaziente di entità nidificate più profonde di un livello. Ad esempio, supponiamo che uno Order abbia la propria collezione di LineItem.

Prima EF7 Credo che la seguente sarebbe stato possibile recuperare anche i LineItems associati a ciascun ordine:

productRepository.GetById(2, p => p.Orders.Select(o => o.LineItems), p => p.Parts); 

Tuttavia, questo non sembra essere supportato in EF7. Invece c'è un nuovo metodo di ThenInclude che recupera ulteriori livelli di entità nidificate:

https://github.com/aspnet/EntityFramework/wiki/Design-Meeting-Notes:-January-8,-2015

Sono incerto su come aggiornare il mio repository per supportare il recupero di molteplici livelli di entità caricati desiderosi utilizzando ThenInclude.

risposta

5

Questo è un po 'di una vecchia questione, ma dal momento che doesn Ho una risposta accettata Ho pensato di pubblicare la mia soluzione a questo.

Sto usando EF Core e volevo fare esattamente questo, accedendo con entusiasmo il caricamento dall'esterno della mia classe di repository in modo da poter specificare le proprietà di navigazione da caricare ogni volta che chiamo un metodo di repository. Dato che ho un gran numero di tabelle e dati, non volevo un set standard di entità caricate avidamente poiché alcune delle mie query richiedevano solo l'entità padre e alcune avevano bisogno dell'intero albero.

mio attuale implementazione supporta solo IQueryable metodo (es. FirstOrDefault, Where, fondamentalmente le funzioni lambda normali), ma sono sicuro che si potrebbe utilizzare per passare attraverso i vostri metodi di repository specifici.

Ho iniziato con il codice sorgente per EF Core EntityFrameworkQueryableExtensions.cs che è dove sono definiti i metodi di estensione Include e ThenInclude. Sfortunatamente, EF utilizza una classe interna IncludableQueryable per contenere l'albero delle proprietà precedenti per consentire di digitare in modo consistente i successivi include. Tuttavia, l'implementazione per questo non è altro che IQueryable con un tipo generico extra per l'entità precedente.

ho creato una mia versione ho chiamato IncludableJoin che prende un IIncludableQueryable come parametro del costruttore e lo memorizza in un campo privato di accesso più tardi:

public interface IIncludableJoin<out TEntity, out TProperty> : IQueryable<TEntity> 
{ 
} 

public class IncludableJoin<TEntity, TPreviousProperty> : IIncludableJoin<TEntity, TPreviousProperty> 
{ 
    private readonly IIncludableQueryable<TEntity, TPreviousProperty> _query; 

    public IncludableJoin(IIncludableQueryable<TEntity, TPreviousProperty> query) 
    { 
     _query = query; 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 

    public IEnumerator<TEntity> GetEnumerator() 
    { 
     return _query.GetEnumerator(); 
    } 

    public Expression Expression => _query.Expression; 
    public Type ElementType => _query.ElementType; 
    public IQueryProvider Provider => _query.Provider; 

    internal IIncludableQueryable<TEntity, TPreviousProperty> GetQuery() 
    { 
     return _query; 
    } 
} 

Annotare il GetQuery metodo interno. Questo sarà importante più tardi.

successiva, nel mio interfaccia generica IRepository, ho definito il punto di partenza per il caricamento ansiosi:

public interface IRepository<TEntity> where TEntity : class 
{ 
    IIncludableJoin<TEntity, TProperty> Join<TProperty>(Expression<Func<TEntity, TProperty>> navigationProperty); 
    ... 
} 

Il tipo generico TEntity è l'interfaccia della mia entità EF. L'implmentation del metodo Join nel mio repository generico è in questo modo:

public abstract class SecureRepository<TInterface, TEntity> : IRepository<TInterface> 
    where TEntity : class, new() 
    where TInterface : class 
{ 
    protected DbSet<TEntity> DbSet; 
    protected SecureRepository(DataContext dataContext) 
    { 
     DbSet = dataContext.Set<TEntity>(); 
    } 

    public virtual IIncludableJoin<TInterface, TProperty> Join<TProperty>(Expression<Func<TInterface, TProperty>> navigationProperty) 
    { 
     return ((IQueryable<TInterface>)DbSet).Join(navigationProperty); 
    } 
    ... 
} 

Ora, per la parte che consente in realtà per più Include e ThenInclude. Ho diversi metodi di estensione che prendono e restituiscono e IIncludableJoin per consentire il concatenamento del metodo. All'interno che io chiamo l'EF Include e ThenInclude metodi sulla DbSet:

public static class RepositoryExtensions 
{ 
    public static IIncludableJoin<TEntity, TProperty> Join<TEntity, TProperty>(
     this IQueryable<TEntity> query, 
     Expression<Func<TEntity, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     return new IncludableJoin<TEntity, TProperty>(query.Include(propToExpand)); 
    } 

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
     this IIncludableJoin<TEntity, TPreviousProperty> query, 
     Expression<Func<TPreviousProperty, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     IIncludableQueryable<TEntity, TPreviousProperty> queryable = ((IncludableJoin<TEntity, TPreviousProperty>)query).GetQuery(); 
     return new IncludableJoin<TEntity, TProperty>(queryable.ThenInclude(propToExpand)); 
    } 

    public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
     this IIncludableJoin<TEntity, IEnumerable<TPreviousProperty>> query, 
     Expression<Func<TPreviousProperty, TProperty>> propToExpand) 
     where TEntity : class 
    { 
     var queryable = ((IncludableJoin<TEntity, IEnumerable<TPreviousProperty>>)query).GetQuery(); 
     var include = queryable.ThenInclude(propToExpand); 
     return new IncludableJoin<TEntity, TProperty>(include); 
    } 
} 

In questi metodi che sto ottenendo il IIncludableQueryable proprietà interna usando il suddetto metodo di GetQuery, chiamando il relativo metodo di Include o ThenInclude, per poi tornare un nuovo IncludableJoin oggetto per supportare il concatenamento del metodo.

E questo è tutto. L'utilizzo di questo è in questo modo:

IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId); 

È possibile che questo sarebbe caricare l'entità di base Account, è uno-a-un bambino Subscription, è uno-a-molti list bambino Addresses ed è figlio Address. Ogni funzione lambda lungo il percorso è fortemente tipizzata ed è supportata da intellisense per mostrare le proprietà disponibili su ciascuna entità.

+0

Ci sarà uno svantaggio delle prestazioni per Joining prima e quindi selezionare (tramite FirstOrDefault() rispetto alla selezione prima? – phhbr

+1

@phhbr Non penso che sia così? Non penso che importi perché quando siedi in un 'IQueryable' il la query non viene eseguita fino a quando non si chiama '.ToList' o simile. Quindi l'ordine delle operazioni probabilmente non ha importanza, ma non l'ho testato. – Steve

6

È possibile modificarlo a qualcosa di simile:

public virtual TEntity GetById<TEntity>(int id, Func<IQueryable<TEntity>, IQueryable<TEntity>> func) 
{ 
    DbSet<TEntity> result = this.Set<TEntity>(); 

    IQueryable<TEntity> resultWithEagerLoading = func(result); 

    return resultWithEagerLoading.FirstOrDefault(e => e.Id == id); 
} 


E si può usare in questo modo:

productRepository.GetById(2, x => x.Include(p => p.Orders) 
            .ThenInclude(o => o.LineItems) 
            .Include(p => p.Parts)) 
+0

Grazie per aver trovato il tempo di rispondere. Il problema che ho con la soluzione proposta è che espone le chiamate IQueryable/Entity Framework al di fuori del repository. Preferisco non farlo. – aw1975

+2

Capisco la tua preoccupazione. Un modo per rendere questa "ignoranza della persistenza" è creare alcuni oggetti nel proprio livello dati per incapsulare i dettagli del recupero (EF Include) e poi farli implementare alcune interfacce. Quindi è possibile passare tale interfaccia al proprio metodo di repository e disporre del repository per risolvere l'oggetto implementato e quindi richiamare gli include all'interno dell'oggetto. È un po 'di lavoro sistemarlo ma è il modo in cui implemento il carico impaziente nei miei progetti. –