24

Dopo aver letto e provato molte cose con l'ultima versione stabile (6.1.1) di Entity Framework.Codice EF6 Prima con repository generico e Dipendenza dell'iniezione e SoC

sto leggendo un sacco di contraddizioni circa se o non utilizzare i repository con EF6 o EF in generale, perché è DbContext fornisce già un repository e DbSet il UoW, fuori dalla scatola.

Lasciatemi prima spiegare cosa contiene la mia soluzione in termini di progetto e poi tornerò alla contraddizione.

Ha un progetto di libreria di classi e un progetto asp.net-mvc. Il progetto class lib è l'accesso ai dati e dove Migrations sono abilitati per Code First.

Nel mio progetto lib classe ho un repository generico:

public interface IRepository<TEntity> where TEntity : class 
{ 
    IEnumerable<TEntity> Get(); 

    TEntity GetByID(object id); 

    void Insert(TEntity entity); 

    void Delete(object id); 

    void Update(TEntity entityToUpdate); 
} 

E qui è la realizzazione di esso:

public class Repository<TEntity> where TEntity : class 
{ 
    internal ApplicationDbContext context; 
    internal DbSet<TEntity> dbSet; 

    public Repository(ApplicationDbContext context) 
    { 
     this.context = context; 
     this.dbSet = context.Set<TEntity>(); 
    } 

    public virtual IEnumerable<TEntity> Get() 
    { 
     IQueryable<TEntity> query = dbSet; 
     return query.ToList(); 
    } 

    public virtual TEntity GetByID(object id) 
    { 
     return dbSet.Find(id); 
    } 

    public virtual void Insert(TEntity entity) 
    { 
     dbSet.Add(entity); 
    } 

    public virtual void Delete(object id) 
    { 
     TEntity entityToDelete = dbSet.Find(id); 
     Delete(entityToDelete); 
    } 

    public virtual void Update(TEntity entityToUpdate) 
    { 
     dbSet.Attach(entityToUpdate); 
     context.Entry(entityToUpdate).State = EntityState.Modified; 
    } 
} 

E qui qualche entità:

public DbSet<User> User{ get; set; } 
public DbSet<Order> Orders { get; set; } 
public DbSet<UserOrder> UserOrders { get; set; } 
public DbSet<Shipment> Shipments { get; set; } 

Non so cosa ripetermi ma, con EF6 non si passano più repository, ma il DbContext invece. Così per DI Ho installato il seguente nel progetto asp-net-mvc utilizzando Ninject:

private static void RegisterServices(IKernel kernel) 
{ 
    kernel.Bind<ApplicationDbContext>().ToSelf().InRequestScope(); 
} 

E questo sarà iniettare il ApplicationDbContext tramite iniezione di costruzione per classi di livello superiore, se del caso.

Ora tornando alla contraddizione.

Se non abbiamo più bisogno di un repository perché EF fornisce già quello pronto, come facciamo Separation of Concern (abbreviato in SoC nel titolo)?

Ora correggimi se ho torto, ma mi sembra che ho solo bisogno di fare tutti i calcoli/calcoli di accesso ai dati (come aggiungere, recuperare, aggiornare, cancellare e qualche logica/calcoli personalizzati qua e là (entità specifica)) nel progetto asp.net-mvc, se non aggiungo un repository.

Qualsiasi luce su questo argomento è molto apprezzata.

+0

E 'solo una questione di come astratto si può ottenere. Il contesto EF * è * in effetti un repository, ma un repository molto ponderato con un proprio insieme di metodi specifici del database relazionale. Se vuoi astrarre anche quello, dovrai avvolgere EF con il tuo repository generico. – haim770

+0

La contraddizione che ho letto su internet è la cosa che mi ha infastidito di più. Ma ora ci siamo chiariti, mi piacerebbe sapere qual è la strada da percorrere. Chris Pratt dice con i servizi. Che ne dici? – Quoter

+0

I servizi sono sempre esistiti (e resi molto popolari a causa dell'influenza del design basato sul dominio) ma non penso che siano destinati a * sostituire * i repository. Per quanto ho capito, sono semplicemente una facciata per azioni e query comuni, e nella maggior parte delle implementazioni vedrai un 'IRepository' che si sta iniettando in esso. Alla fine, tutto dipende dallo scopo del tuo progetto. Dovete chiedervi quanto sia importante per il progetto il livello di astrazione che scegliete e quanto esattamente lo influenzerà a lungo termine, specialmente per quanto riguarda la testabilità e la scalabilità. – haim770

risposta

37

Spero che una piccola spiegazione chiarisca la tua confusione. Il pattern del repository esiste per astrarre la connessione al database e la logica di interrogazione. Gli ORM (mappatori di oggetti relazionali, come EF) sono stati in giro in una forma o nell'altra da così tanto tempo che molte persone hanno dimenticato o non hanno mai avuto l'immensa gioia e il piacere di occuparsi del codice spaghetti disseminato di query SQL e dichiarazioni. Il tempo era che se volevi interrogare un database, eri in realtà responsabile di cose pazzesche come l'avvio di una connessione e la costruzione di istruzioni SQL dall'etere. Il punto del modello di repository era quello di darti un posto unico per mettere tutta questa cattiveria, lontano dal tuo bellissimo codice applicativo.

Avanti veloce al 2014, Entity Framework e altri ORM sono il repository.Tutta la logica SQL è impacchettata ordinatamente lontano dai tuoi occhi indiscreti, e invece hai una bella API programmatica da usare nel tuo codice. Da un punto di vista, è abbastanza astrazione. L'unica cosa che non copre è la dipendenza dall'ORM stesso. Se in seguito decidi di voler disattivare Entity Framework per qualcosa di simile a NHibernate o persino a un'API Web, devi eseguire un'operazione sulla tua applicazione per farlo. Di conseguenza, aggiungere un altro livello di astrazione è comunque una buona idea, ma non un repository, o almeno un tipico repository.

Il repository di cui si dispone è un repository tipico. Crea semplicemente proxy per i metodi dell'API di Entity Framework. Chiami repo.Add e il repository chiama context.Add. È, francamente, ridicolo, ed è per questo che molti, me compreso, dicono che non usano repository con Entity Framework.

Quindi cosa deve fare ? Creare servizi, o forse è meglio dire come "classi di servizio". Quando i servizi iniziano a essere discussi in relazione a .NET, all'improvviso stai parlando di tutti i tipi di cose che sono completamente irrilevanti rispetto a ciò di cui stiamo discutendo qui. Una classe simile al servizio è come un servizio in quanto ha endpoint che restituiscono un particolare insieme di dati o eseguono una funzione molto specifica su alcuni set di dati. Per esempio, mentre con un repository tipica si dovrebbe trovare la vostra auto facendo le cose come:

articleRepo.Get().Where(m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now).OrderByDescending(o => o.PublishDate) 

La classe di servizio dovrebbe funzionare come:

service.GetPublishedArticles(); 

Sede, tutta la logica per quello che si qualifica come "pubblicato l'articolo "è ben contenuto nel metodo endpoint. Inoltre, con un repository, stai ancora esponendo l'API sottostante. È più facile passare con qualcos'altro perché il datastore di base è astratto, ma se l'API per l'interrogazione in quell'archivio cambia sei ancora su un torrente.

UPDATE

Impostare sarebbe molto simile; la differenza è principalmente nella modalità di utilizzo di un servizio rispetto a un repository. Vale a dire, non lo renderei nemmeno dipendente dall'entità. In altre parole, avresti essenzialmente un servizio per contesto, non per entità.

Come sempre, inizia con un'interfaccia:

public interface IService 
{ 
    IEnumerable<Article> GetPublishedArticles(); 

    ... 
} 

Poi, l'implementazione:

public class EntityFrameworkService<TContext> : IService 
    where TContext : DbContext 
{ 
    protected readonly TContext context; 

    public EntityFrameworkService(TContext context) 
    { 
     this.context = context; 
    } 

    public IEnumerable<Article> GetPublishedArticles() 
    { 
     ... 
    } 
} 

Poi, le cose cominciano a diventare un po 'pelosa. Nel metodo di esempio, è possibile fare semplicemente riferimento allo DbSet direttamente, ad esempio context.Articles, ma ciò implica la conoscenza dei nomi DbSet nel contesto. È meglio usare context.Set<TEntity>(), per una maggiore flessibilità. Prima di saltare troppo, però, voglio sottolineare perché ho chiamato questo EntityFrameworkService. Nel tuo codice, ti riferiresti sempre solo all'interfaccia IService. Quindi, tramite il contenitore di iniezione delle dipendenze, è possibile sostituire EntityFrameworkService<YourContext> per quello. Questo apre la possibilità di creare altri fornitori di servizi come forse

Ora, mi piace utilizzare un singolo metodo protetto che restituisce un interrogabile che tutti i miei metodi di servizio possono utilizzare. Questo elimina un sacco di problemi come dover inizializzare un'istanza DbSet ogni volta tramite var dbSet = context.Set<YourEntity>();.Che starebbe un po 'come:

protected virtual IQueryable<TEntity> GetQueryable<TEntity>(
    Expression<Func<TEntity, bool>> filter = null, 
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, 
    string includeProperties = null, 
    int? skip = null, 
    int? take = null) 
    where TEntity : class 
{ 
    includeProperties = includeProperties ?? string.Empty; 
    IQueryable<TEntity> query = context.Set<TEntity>(); 

    if (filter != null) 
    { 
     query = query.Where(filter); 
    } 

    foreach (var includeProperty in includeProperties.Split 
     (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 
    { 
     query = query.Include(includeProperty); 
    } 

    if (orderBy != null) 
    { 
     query = orderBy(query); 
    } 

    if (skip.HasValue) 
    { 
     query = query.Skip(skip.Value); 
    } 

    if (take.HasValue) 
    { 
     query = query.Take(take.Value); 
    } 

    return query; 
} 

Si noti che questo metodo è, in primo luogo, protetto. Le sottoclassi possono utilizzarlo, ma questo dovrebbe sicuramente non essere parte dell'API pubblica. Il punto centrale di questo esercizio è quello di non esporre queryable. In secondo luogo, è generico. In altre parole, è in grado di gestire qualsiasi tipo tu ci passi finché c'è qualcosa nel contesto per esso.

Poi, nel nostro metodo ad esempio poco, che ci si finisce per fare qualcosa di simile:

public IEnumerable<Article> GetPublishedArticles() 
{ 
    return GetQueryable<Article>(
     m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now, 
     m => m.OrderByDescending(o => o.PublishDate) 
    ).ToList(); 
} 

Un altro trucco per questo approccio è la possibilità di avere metodi di servizio generici che utilizzano interfacce. Diciamo che volevo essere in grado di avere un metodo per ottenere uno pubblicato nulla. Potrei avere un'interfaccia come:

public interface IPublishable 
{ 
    PublishStatus Status { get; set; } 
    DateTime PublishDate { get; set; } 
} 

Quindi, qualsiasi entità che è pubblicabile implementerebbe solo questa interfaccia. Con questo in luogo, ora è possibile farlo:

public IEnumerable<TEntity> GetPublished<TEntity>() 
    where TEntity : IPublishable 
{ 
    return GetQueryable<TEntity>(
     m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now, 
     m => m.OrderByDescending(o => o.PublishDate) 
    ).ToList(); 
} 

E poi nel codice dell'applicazione:

service.GetPublished<Article>(); 
+0

Grazie mille per la spiegazione dettagliata. Dopo averlo letto alcune volte, ho ancora alcune domande. Come creo/utilizzo l'oggetto 'service'? Mi piacerebbe tutto accoppiato liberamente, senza dipendenze. Un esempio di codice/progetto sarebbe carino. Pensavo, ne ho cercato uno dopo aver letto il tuo post, ma non ho trovato nessuno che non usi repository. – Quoter

+0

Vedere il mio aggiornamento sopra. –

+0

Grazie, ancora, molto bello e dettagliato spiegato. Il modo in cui ho usato 'DI' con' Ninject', è la strada da seguire anche nel tuo esempio? – Quoter