24

Nach einer Menge Lesen und Ausprobieren mit Entity Framework neuesten stabilen Version (6.1.1).EF6 Code zuerst mit generischem Repository und Dependency Injection und SoC

Ich lese viele Widersprüche darüber, ob oder nicht Repositories mit EF6 oder EF im Allgemeinen zu verwenden, weil es DbContext bietet bereits ein Repository und DbSet die UoW, aus dem Kasten heraus.

Lassen Sie mich zuerst erklären, was meine Lösung in Bezug auf das Projekt enthält, und dann werde ich am Widerspruch zurückkommen.

Es hat ein Klassenbibliotheksprojekt und ein asp.net-mvc Projekt. Das Class-Lib-Projekt ist der Datenzugriff, wobei Migrations für Code First aktiviert sind.

In meiner Klasse lib Projekt habe ich eine generische Repository:

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

Und hier ist die Implementierung davon:

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

Und hier ein paar Einheiten:

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

Ich weiß nicht, was ich wiederholen soll, aber mit EF6 übergeben Sie keine Repositories mehr, sondern die DbContext statt. Also für DI ich folgend im asp-net-mvc Projekt mit Ninject eingerichtet habe:

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

Und dies wird die ApplicationDbContext über Konstruktor Injektion obere Schicht Klassen ggf. injiziert.

Kommen wir nun zum Widerspruch zurück.

Wenn wir kein Repository mehr benötigen, weil EF bereits das out of the box bietet, wie machen wir Separation of Concern (abgekürzt als SoC in Titel)?

Korrigieren Sie mich jetzt, wenn ich falsch liege, aber es klingt für mich wie ich muss nur die Datenzugriffslogik/Berechnungen (wie hinzufügen, holen, aktualisieren, löschen und einige benutzerdefinierte Logik/Berechnungen hier und da (Entität)) im Projekt asp.net-mvc, wenn ich kein Repository hinzufügen.

Jedes Licht in dieser Angelegenheit ist sehr geschätzt.

+0

Es ist nur eine Frage, wie abstrakt man bekommen kann. EF context * ist * in der Tat ein Repository, aber ein sehr eigenwilliges Repository mit eigenen relationaldatenbankspezifischen Methoden. Wenn Sie das auch abstrahieren möchten, müssen Sie EF mit einem eigenen generischen Repository versehen. – haim770

+0

Der Widerspruch, den ich im Internet gelesen habe, hat mich am meisten gestört. Aber jetzt haben wir das aufgeklärt, ich würde gerne wissen, wie es weiter geht. Chris Pratt sagt mit Dienstleistungen. Was sagst du? – Quoter

+0

Dienste waren schon immer da (und wurden aufgrund des Einflusses von Domain-Driven Design populär), aber ich denke nicht, dass sie * Repositories ersetzen sollen. Soweit ich weiß, sind sie nur eine Fassade für gemeinsame Aktionen und Abfragen, und in den meisten Implementierungen sehen Sie ein "IRepository", das injiziert wird. Am Ende hängt alles vom Umfang Ihres Projekts ab. Sie müssen sich die Frage stellen, wie wichtig das gewählte Abstraktionsniveau für das Projekt ist und wie genau es sich langfristig auf dieses Projekt auswirken wird, insbesondere in Bezug auf Testbarkeit und Skalierbarkeit. – haim770

Antwort

37

Eine kleine Erklärung wird hoffentlich Ihre Verwirrung klären. Das Repository-Muster existiert, um Datenbankverbindungs- und Abfragelogik zu abstrahieren. ORMs (objektrelationale Mapper, wie EF) gibt es in der einen oder anderen Form so lange, dass viele Menschen die immense Freude und Freude am Umgang mit Spaghetti-Code, der mit SQL-Abfragen und -Abschlüssen übersät ist, vergessen oder nie gehabt haben. Zeit war, dass Sie, wenn Sie eine Datenbank abfragen wollten, tatsächlich für verrückte Dinge verantwortlich waren, wie zum Beispiel eine Verbindung herzustellen und SQL-Anweisungen tatsächlich aus Ether zu konstruieren. Der Sinn des Repository-Musters bestand darin, Ihnen einen einzigen Ort zu geben, an dem Sie all diese Gemeinheiten von Ihrem schönen, ursprünglichen Anwendungscode entfernen können.

Vorspulen auf 2014, Entity Framework und andere ORMs sind Ihr Repository.Die gesamte SQL-Logik ist sauber von Ihren neugierigen Augen entfernt und Sie haben stattdessen eine nette programmatische API, die Sie in Ihrem Code verwenden können. In einer Hinsicht ist das genug Abstraktion. Das einzige, was es nicht abdeckt, ist die Abhängigkeit vom ORM selbst. Wenn Sie später entscheiden, dass Sie Entity Framework für etwas wie NHibernate oder sogar eine Web-API deaktivieren möchten, müssen Sie eine Operation für Ihre Anwendung durchführen, um dies zu tun. Daher ist das Hinzufügen einer weiteren Abstraktionsebene immer noch eine gute Idee, aber kein Repository oder zumindest ein typisches Repository.

Das Repository, das Sie haben, ist ein typisches Repository. Es erstellt lediglich Proxies für die Entity Framework-API-Methoden. Sie rufen repo.Add und das Repository ruft context.Add. Es ist, ehrlich gesagt, lächerlich, und deshalb verwenden viele, einschließlich mir selbst, sagen nicht Repositories mit Entity Framework.

Also was sollte Sie tun? Erstellen Sie Dienste, oder vielleicht wird es am besten als "service-like-Klassen" bezeichnet. Wenn Services in Bezug auf .NET diskutiert werden, sprechen Sie plötzlich über alle möglichen Dinge, die für das, was wir hier diskutieren, völlig irrelevant sind. Eine serviceähnliche Klasse ist wie ein Service insofern, als sie Endpunkte aufweist, die einen bestimmten Datensatz zurückgeben oder eine bestimmte Funktion für einen bestimmten Datensatz ausführen. Während zum Beispiel mit einem typischen Repository würden Sie sich selbst Dinge zu tun, wie finden:

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

Ihre Serviceklasse funktionieren würde wie:

service.GetPublishedArticles(); 

See, die gesamte Logik für das, was als eine „veröffentlichte qualifiziert Artikel "ist sauber in der Endpunktmethode enthalten. Außerdem stellen Sie mit einem Repository die zugrunde liegende API offen. Es ist einfacher mit etwas anderes zu wechseln, da der Basisdatenspeicher abstrahiert ist, aber wenn die API für die Abfrage in diesem Datenspeicher ändert, sind Sie immer noch ein Bach.

UPDATE

einrichten wäre sehr ähnlich; Der Unterschied besteht hauptsächlich darin, wie Sie einen Service gegenüber einem Repository verwenden. Nämlich, ich würde es nicht einmal abhängig machen. Mit anderen Worten, Sie haben im Wesentlichen einen Service pro Kontext, nicht pro Entität.

Wie immer mit einer Schnittstelle starten:

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

    ... 
} 

Dann Implementierung ab:

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

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

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

Dann Dinge beginnen, ein wenig haarig zu bekommen. In der Beispielmethode könnten Sie einfach direkt auf die DbSet verweisen, d. H. context.Articles, aber das impliziert Wissen über die DbSet Namen in dem Zusammenhang. Es ist besser, context.Set<TEntity>() für mehr Flexibilität zu verwenden. Bevor ich jedoch zu viel auf Züge zusteuere, möchte ich darauf hinweisen, warum ich diesen Namen EntityFrameworkService genannt habe. In Ihrem Code würden Sie nur auf Ihre IService Schnittstelle verweisen. Dann können Sie über Ihren Dependency-Injektionscontainer EntityFrameworkService<YourContext> dafür ersetzen. Dies eröffnet die Möglichkeit, andere Dienstanbieter wie vielleicht WebApiService, etc. zu erstellen

Jetzt möchte ich eine einzige geschützte Methode verwenden, die eine Abfrage zurückgibt, die alle meine Dienstmethoden verwenden können. Dies wird eine Menge von der cruft wie eine DbSet Instanz jedes Mal über var dbSet = context.Set<YourEntity>(); initialisieren müssen loswerden.Das würde ein bisschen wie aussehen:

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

Beachten Sie, dass diese Methode zuerst geschützt ist. Unterklassen können es verwenden, aber dies sollte definitiv nicht Teil der öffentlichen API sein. Der springende Punkt dieser Übung besteht darin, keine abfragbaren Daten verfügbar zu machen. Zweitens ist es generisch. Mit anderen Worten, es kann mit jedem Typ umgehen, den Sie werfen, solange etwas im Kontext dafür ist.

Dann in unserem kleinen Beispiel Verfahren, würden Sie wie etwas zu tun, am Ende:

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

Ein weiterer netter Trick dieses Ansatzes ist die Fähigkeit, generische Service-Methoden unter Verwendung von Schnittstellen. Sagen wir, ich wollte in der Lage sein, eine Methode zu haben, um eine veröffentlichte irgendetwas zu bekommen. Ich könnte eine Schnittstelle wie haben:

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

Dann alle Entitäten, die publishable sind, würde nur diese Schnittstelle implementieren. Mit dem im Ort, können Sie jetzt tun:

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

Und dann im Anwendungscode:

service.GetPublished<Article>(); 
+0

Vielen Dank für Ihre ausführliche Erklärung. Nachdem ich es ein paar Mal gelesen habe, habe ich noch einige Fragen. Wie würde ich das 'service' Objekt erstellen/verwenden? Ich möchte, dass alles lose gekoppelt ist, keine Abhängigkeiten. Ein Beispielcode/Projekt wäre nett. Dachte, ich habe nach dem Lesen Ihres Beitrags gesucht, konnte einfach keine finden, die keine Repositories verwenden. – Quoter

+0

Siehe mein Update oben. –

+0

Danke nochmal sehr nett und ausführlich erklärt. Wie ich 'DI' mit' Ninject' benutzt habe, ist das auch der Weg in deinem Beispiel? – Quoter