c # -Entity-Framework: Richtige Verwendung der DBContext-Klasse in Ihrer Repository-Klasse


73

Ich habe meine Repository-Klassen implementiert, wie Sie unten sehen können

public Class MyRepository
{
      private MyDbContext _context; 

      public MyRepository(MyDbContext context)
      {
          _context = context;
      }

      public Entity GetEntity(Guid id)
      {
          return _context.Entities.Find(id);
      }
}

Ich habe jedoch kürzlich diesen Artikel gelesen, der besagt, dass es eine schlechte Praxis ist, Datenkontext als privates Mitglied in Ihrem Repository zu haben: http://devproconnections.com/development/solving-net-scalability-problem

Theoretisch ist der Artikel nun richtig: Da DbContext IDisposable implementiert, ist die korrekteste Implementierung die folgende.

public Class MyRepository
{
      public Entity  GetEntity(Guid id)
      {
          using (MyDbContext context = new MyDBContext())
          {
              return context.Entities.Find(id);
          }
      }
}

Laut diesem anderen Artikel wäre das Entsorgen von DbContext jedoch nicht unbedingt erforderlich: http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html

Welcher der beiden Artikel ist richtig? Ich bin ziemlich verwirrt. DbContext als privates Mitglied in Ihrer Repository-Klasse zu haben, kann wirklich "Skalierbarkeitsprobleme" verursachen, wie der erste Artikel vorschlägt?


2
Ich habe immer verstanden, dass DBContext nur für eine Arbeitseinheit geöffnet sein sollte
Mark Homer

Bevorzugen Sie also den zweiten Code?
Errore Fatale

Es sieht für mich ja besser aus
Mark Homer

2
Bisher zehn Antworten. "Erster Ansatz", "zweiter Ansatz", "ein anderer Ansatz". Dies ist ein Lehrbuchbeispiel für eine hauptsächlich meinungsbasierte Frage. Ich würde dafür stimmen, es zu schließen, wenn es nicht das Kopfgeld gäbe.
Gert Arnold

12
@Gert Arnold wie ist das eine meinungsbasierte frage? Die Frage, wie eine Sprache oder ein Framework richtig verwendet werden kann, ist keine Meinung.
Errore Fatale

Antworten:


41

Ich denke, Sie sollten dem ersten Artikel nicht folgen , und ich werde Ihnen sagen, warum.

Nach dem ersten Ansatz verlieren Sie so gut wie alle Funktionen, die Entity Framework über das Programm bereitstellt DbContext, einschließlich des Caches der ersten Ebene, der Identitätszuordnung, der Arbeitseinheit sowie der Funktionen zur Änderungsverfolgung und zum verzögerten Laden. Dies liegt daran, dass im obigen Szenario DbContextfür jede Datenbankabfrage eine neue Instanz erstellt und unmittelbar danach entsorgt wird, wodurch verhindert wird, dass die DbContextInstanz den Status Ihrer Datenobjekte über den gesamten Geschäftsvorgang hinweg verfolgen kann.

Ein DbContextals Privateigenschaft in Ihrer Repository-Klasse zu haben, hat auch seine Probleme. Ich glaube, der bessere Ansatz ist ein CustomDbContextScope. Dieser Ansatz wird von diesem Typen sehr gut erklärt: Mehdi El Gueddari

Dieser Artikel http://mehdi.me/ambient-dbcontext-in-ef6/ ist einer der besten Artikel über EntityFramework, die ich gesehen habe. Sie sollten es vollständig lesen, und ich glaube, es wird alle Ihre Fragen beantworten.


+1, der von Ihnen angegebene Link beschreibt einen interessanten Ansatz. Es bietet eine solide Erklärung der Herausforderungen sowie eine schöne Implementierung einer Lösung. Ich hatte noch keine Gelegenheit, es zu testen, aber die Idee, verschachtelte Kontextbereiche verwenden zu können, ohne Transaktionsbereiche verwenden zu müssen, ist großartig.
Ken2k

1
Der in diesem Artikel von mehdi.me beschriebene Ansatz geht von vielen Annahmen aus, die meiner Meinung nach für viele Anwendungen möglicherweise falsch sind (nur ein Aufruf von SaveChanges ist zulässig, schlechte Transaktionsabwicklung). Es gibt viele gute Dinge in seinem vorgeschlagenen Ansatz und seiner Implementierung, aber wir mussten ihn stark modifizieren.
BenCr

Normalerweise ist ein Aufruf von SaveChanges () der richtige Weg, um Dinge mit EF6 zu tun. Aber natürlich ist jedes Szenario anders und wir müssen möglicherweise manchmal Regeln ändern. Ich habe nur versucht, eine "allgemeinere" Antwort zu geben :)
Fabio Luz

Sie sollten den zweiten Absatz in ein Blockzitat setzen, wenn Sie einen Teil des verlinkten Artikels, Tippfehler und alles direkt kopieren
möchten

19

Angenommen, Sie haben mehr als ein Repository und müssen zwei Datensätze aus verschiedenen Repositorys aktualisieren. Und Sie müssen es transaktional tun (wenn einer fehlschlägt - beide Updates rollen zurück):

var repositoryA = GetRepository<ClassA>();
var repositoryB = GetRepository<ClassB>();

repository.Update(entityA);
repository.Update(entityB);

Wenn Sie also für jedes Repository einen eigenen DbContext haben (Fall 2), müssen Sie TransactionScope verwenden, um dies zu erreichen.

Besser - haben Sie einen gemeinsamen DbContext für eine Operation (für einen Anruf, für eine Arbeitseinheit ). So kann DbContext Transaktionen verwalten. EF ist genau dafür. Sie können nur einen DbContext erstellen, alle Änderungen in vielen Repositorys vornehmen, SaveChanges einmal aufrufen und nach Abschluss aller Vorgänge und Arbeiten entsorgen.

Hier ist ein Beispiel für die Implementierung eines UnitOfWork-Musters.

Ihr zweiter Weg kann für schreibgeschützte Operationen gut sein.


4
Dies ist das Aushängeschild dafür, warum ein Repository ein fehlgeschlagenes Konzept ist. Sagen Sie einfach Nein zu einem Repo pro Unternehmen.
Jeff Dunlop

9
Wie ist das? UnitOfWork verwendet weiterhin das Konzept "Repository pro Entität". Wenn überhaupt, zeigen diese Beispiele Controller, die viel zu viel Arbeit leisten. Es sollte eine Service-Schicht geben, die all diese Arbeit erledigt, und die UoW sollte sich innerhalb der Service-Schicht befinden. Controller sollten dünn und dumm sein.
user441521

13

Die Grundregel lautet: Ihre DbContext-Lebensdauer sollte auf die Transaktion beschränkt sein, die Sie ausführen .

Hier kann sich "Transaktion" auf eine schreibgeschützte Abfrage oder eine Schreibabfrage beziehen. Und wie Sie vielleicht bereits wissen, sollte eine Transaktion so kurz wie möglich sein.

Trotzdem würde ich sagen, dass Sie in den meisten Fällen die "Verwendung" bevorzugen und kein privates Mitglied verwenden sollten.

Der einzige Fall, in dem ich sehe, dass ein privates Mitglied verwendet wird, betrifft ein CQRS-Muster (CQRS: Eine Kreuzprüfung der Funktionsweise) .

Übrigens gibt die Antwort von Diego Vega im Beitrag von Jon Gallant auch einige weise Ratschläge:

Es gibt zwei Hauptgründe, warum unser Beispielcode immer "using" verwendet oder den Kontext auf andere Weise entsorgt:

  1. Das standardmäßige automatische Öffnen / Schließen-Verhalten ist relativ einfach zu überschreiben: Sie können die Kontrolle darüber übernehmen, wann die Verbindung geöffnet und geschlossen wird, indem Sie die Verbindung manuell öffnen. Sobald Sie dies in einem Teil Ihres Codes tun, wird das Vergessen, den Kontext zu löschen, schädlich, da Sie möglicherweise offene Verbindungen verlieren.

  2. DbContext implementiert IDiposable nach dem empfohlenen Muster, einschließlich der Bereitstellung einer virtuellen geschützten Dispose-Methode, die abgeleitete Typen überschreiben können, wenn beispielsweise andere nicht verwaltete Ressourcen in der Lebensdauer des Kontexts zusammengefasst werden müssen.

HTH


5

Welcher Ansatz verwendet werden soll, hängt von der Verantwortung des Repositorys ab.

Ist das Repository dafür verantwortlich, vollständige Transaktionen auszuführen? dh um Änderungen vorzunehmen und dann Änderungen an der Datenbank zu speichern, indem Sie `SaveChanges? Oder ist es nur ein Teil einer größeren Transaktion und nimmt daher nur Änderungen vor, ohne sie zu speichern?

Fall 1) Das Repository führt vollständige Transaktionen aus (es nimmt Änderungen vor und speichert sie):

In diesem Fall ist der zweite Ansatz besser (der Ansatz Ihres zweiten Codebeispiels).

Ich werde diesen Ansatz nur geringfügig ändern, indem ich eine Fabrik wie diese einführe:

public interface IFactory<T>
{
    T Create();
}

public class Repository : IRepository
{
    private IFactory<MyContext> m_Factory;

    public Repository(IFactory<MyContext> factory)
    {
        m_Factory = factory;
    }

    public void AddCustomer(Customer customer)
    {
        using (var context = m_Factory.Create())
        {
            context.Customers.Add(customer);
            context.SaveChanges();
        }            
    }
}

Ich mache diese geringfügige Änderung, um die Abhängigkeitsinjektion zu aktivieren . Dies ermöglicht es uns, später die Art und Weise zu ändern, wie wir den Kontext erstellen.

Ich möchte nicht, dass das Repository die Verantwortung dafür trägt, den Kontext selbst zu erstellen. Die Fabrik, die implementiert IFactory<MyContext>, hat die Verantwortung, den Kontext zu erstellen.

Beachten Sie, wie das Repository die Lebensdauer des Kontexts verwaltet, den Kontext erstellt, einige Änderungen vornimmt, die Änderungen speichert und dann den Kontext entsorgt. Das Repository hat in diesem Fall eine längere Lebensdauer als der Kontext.

Fall 2) Das Repository ist Teil einer größeren Transaktion (es nimmt einige Änderungen vor, andere Repositorys nehmen andere Änderungen vor und dann wird jemand anderes die Transaktion durch Aufrufen festschreiben SaveChanges):

In diesem Fall ist der erste Ansatz (den Sie zuerst in Ihrer Frage beschreiben) besser.

Stellen Sie sich vor, Sie werden verstehen, wie das Repository Teil einer größeren Transaktion sein kann:

using(MyContext context = new MyContext ())
{
    repository1 = new Repository1(context);
    repository1.DoSomething(); //Modify something without saving changes
    repository2 = new Repository2(context);
    repository2.DoSomething(); //Modify something without saving changes

    context.SaveChanges();
}

Bitte beachten Sie, dass für jede Transaktion eine neue Instanz des Repositorys verwendet wird. Dies bedeutet, dass die Lebensdauer des Repositorys sehr kurz ist.

Bitte beachten Sie, dass ich das Repository in meinem Code neu einstelle (was eine Verletzung der Abhängigkeitsinjektion darstellt). Ich zeige dies nur als Beispiel. In echtem Code können wir Fabriken verwenden, um dies zu lösen.

Eine Verbesserung, die wir bei diesem Ansatz vornehmen können, besteht darin, den Kontext hinter einer Schnittstelle auszublenden, sodass das Repository keinen Zugriff mehr hat SaveChanges(siehe das Prinzip der Schnittstellentrennung ).

Sie können so etwas haben:

public interface IDatabaseContext
{
    IDbSet<Customer> Customers { get; }
}

public class MyContext : DbContext, IDatabaseContext
{
    public IDbSet<Customer> Customers { get; set; }
}


public class Repository : IRepository
{
    private IDatabaseContext m_Context;

    public Repository(IDatabaseContext context)
    {
        m_Context = context;
    }

    public void AddCustomer(Customer customer)
    {
        m_Context.Customers.Add(customer);      
    }
}

Sie können der Schnittstelle weitere Methoden hinzufügen, die Sie benötigen, wenn Sie möchten.

Bitte beachten Sie auch, dass diese Schnittstelle nicht von erbt IDisposable. Dies bedeutet, dass die RepositoryKlasse nicht für die Verwaltung der Lebensdauer des Kontexts verantwortlich ist. Der Kontext hat in diesem Fall eine längere Lebensdauer als das Repository. Jemand anderes wird die Lebensdauer des Kontexts verwalten.

Anmerkungen zum ersten Artikel:

Der erste Artikel schlägt vor, dass Sie nicht den ersten Ansatz verwenden sollten, den Sie in Ihrer Frage beschreiben (fügen Sie den Kontext in das Repository ein).

Der Artikel ist nicht klar, wie das Repository verwendet wird. Wird es als Teil einer einzelnen Transaktion verwendet? Oder umfasst es mehrere Transaktionen?

Ich vermute (ich bin mir nicht sicher), dass in dem Ansatz, den der Artikel beschreibt (negativ), das Repository als langjähriger Dienst verwendet wird, der viele Transaktionen umfasst. In diesem Fall stimme ich dem Artikel zu.

Was ich hier vorschlage, ist anders. Ich schlage vor, dass dieser Ansatz nur in dem Fall verwendet wird, in dem jedes Mal, wenn eine Transaktion benötigt wird, eine neue Instanz des Repositorys erstellt wird.

Anmerkungen zum zweiten Artikel:

Ich denke, dass das, worüber der zweite Artikel spricht, für den Ansatz, den Sie verwenden sollten, irrelevant ist.

Im zweiten Artikel wird diskutiert, ob es auf jeden Fall erforderlich ist, den Kontext zu entsorgen (irrelevant für das Design des Repositorys).

Bitte beachten Sie, dass wir in den beiden Entwurfsansätzen über den Kontext verfügen. Der einzige Unterschied besteht darin, wer für eine solche Entsorgung verantwortlich ist.

Der Artikel sagt, dass die DbContextRessourcen zu bereinigen scheinen, ohne dass der Kontext explizit verfügbar gemacht werden muss.


Wie verwenden Sie IDatabaseContextinnerhalb des Repositorys, wenn es keine DbContextMethoden enthält , zum Beispiel als context.Database.ExecuteSqlCommand ?
Muflix

4

Der erste Artikel, den Sie verlinkt haben, hat genau eine wichtige Sache vergessen: die Lebensdauer der sogenannten NonScalableUserRepostoryInstanzen (es wurde auch vergessen, auch NonScalableUserRepostoryImplementierungen IDisposablevorzunehmen, um die DbContextInstanz ordnungsgemäß zu entsorgen ).

Stellen Sie sich folgenden Fall vor:

public string SomeMethod()
{
    using (var myRepository = new NonScalableUserRepostory(someConfigInstance))
    {
        return myRepository.GetMyString();
    }
}

Nun ... es würde noch einige privat sein DbContextFeld innerhalb der NonScalableUserRepostoryKlasse, aber der Kontext wird nur verwendet werden , wenn . Es ist also genau das Gleiche wie das, was der Artikel als Best Practice beschreibt.

Die Frage lautet also nicht " Soll ich ein privates Mitglied gegen eine using-Anweisung verwenden? ", Sondern " Was sollte die Lebensdauer meines Kontexts sein? ".

Die Antwort wäre dann: Versuchen Sie, es so weit wie möglich zu verkürzen. Es gibt den Begriff der Arbeitseinheit , der einen Geschäftsbetrieb darstellt. Grundsätzlich sollten Sie DbContextfür jede Arbeitseinheit eine neue haben .

Wie eine Arbeitseinheit definiert und wie sie implementiert wird, hängt von der Art Ihrer Anwendung ab. Beispielsweise ist für eine ASP.Net MVC-Anwendung die Lebensdauer von im DbContextAllgemeinen die Lebensdauer von HttpRequest, dh jedes Mal, wenn der Benutzer eine neue Webanforderung generiert, wird ein neuer Kontext erstellt.


EDIT:

Um Ihren Kommentar zu beantworten:

Eine Lösung wäre, über den Konstruktor eine Fabrikmethode zu injizieren. Hier ist ein grundlegendes Beispiel:

public class MyService : IService
{
    private readonly IRepositoryFactory repositoryFactory;

    public MyService(IRepositoryFactory repositoryFactory)
    {
        this.repositoryFactory = repositoryFactory;
    }

    public void BusinessMethod()
    {
        using (var repo = this.repositoryFactory.Create())
        {
            // ...
        }
    }
}

public interface IRepositoryFactory
{
    IRepository Create();
}

public interface IRepository : IDisposable
{
    //methods
}

Es gibt jedoch ein Problem: Wenn man bedenkt, dass das Repository höchstwahrscheinlich von der Business-Schicht verwendet wird, und wenn man bedenkt, dass die Business-Schicht der Teil der Anwendung ist, den Sie testen möchten ... wie können Sie ein gefälschtes Repository zum Testen einfügen, wenn Die Instanz wird in jeder Methode erstellt, anstatt injiziert zu werden.
Errore Fatale

@ErroreFatale Ich habe ein Beispiel hinzugefügt, um diese Frage zu beantworten
Ken2k

Übrigens an die Person, die herabgestimmt hat: Versuchen Sie zumindest zu erklären, warum
Ken2k

danke, ich habe die Schnittstelle IRepository zu Ihrem Code-Snippet hinzugefügt ... es ist wichtig zu beachten, dass IRepository IDisposable sein muss. Wenn etwas nicht stimmt, können Sie es korrigieren.
Errore Fatale

@ErroreFatale Ja, es ist wichtig, dass ich, da ich die usingAnweisung verwendet IRepositoryhabe, wegwerfbar sein muss. Vielen Dank für die Bearbeitung
Ken2k

2

Der erste Code hat keine Beziehung zum Scability-Problem. Der Grund dafür ist, dass er für jedes fehlerhafte Repository einen neuen Kontext erstellt, den einer der Kommentatoren kommentiert, aber nicht einmal geantwortet hat. Im Web war es 1 Anfrage 1 dbContext. Wenn Sie ein Repository-Muster verwenden möchten, wird dies in 1 Anfrage> viele Repositorys> 1 dbContext übersetzt. Dies ist mit IoC leicht zu erreichen, aber nicht erforderlich. So geht's ohne IoC:

var dbContext = new DBContext(); 
var repository = new UserRepository(dbContext); 
var repository2 = new ProductRepository(dbContext);
// do something with repo

Was das Entsorgen betrifft oder nicht, entsorge ich es normalerweise, aber wenn das Blei selbst dies sagte, dann wahrscheinlich kein Grund, es zu tun. Ich mag es einfach zu entsorgen, wenn es IDisposable hat.


1

Grundsätzlich ist die DbContext-Klasse nichts anderes als ein Wrapper, der alle datenbankbezogenen Dinge wie Folgendes behandelt: 1. Verbindung erstellen 2. Abfrage ausführen. Wenn wir nun die oben genannten Dinge mit normalem ado.net ausführen, müssen wir die Verbindung explizit ordnungsgemäß schließen, indem wir entweder den Code in using-Anweisung schreiben oder die Methode close () für das Verbindungsklassenobjekt aufrufen.

Da die Kontextklasse die IDisposable-Schnittstelle intern implementiert, empfiehlt es sich, den dbcontext in using-Anweisung zu schreiben, damit wir uns nicht darum kümmern müssen, die Verbindung zu schließen.

Vielen Dank.


Ich stimme Ihren Argumenten nicht zu (würde aber auch das usingMuster empfehlen). DbContextkümmert sich auch um Caching, Änderungsverfolgung und vieles mehr. Ich bin mir nicht ganz sicher, aber ich denke, die Verbindung wird bei jedem Datenbankanruf geöffnet und geschlossen.
xum59

0

Ich benutze den ersten Weg (Injizieren des dbContext) Natürlich sollte es ein IMyDbContext sein und Ihre Abhängigkeitsinjektions-Engine verwaltet den Lebenszyklus des Kontexts, sodass er nur live ist, während er benötigt wird.

Auf diese Weise können Sie den Kontext zum Testen verspotten. Der zweite Weg macht es unmöglich, die Entitäten ohne Datenbank für den zu verwendenden Kontext zu untersuchen.


0

Der zweite Ansatz (Verwenden) ist besser, da er die Verbindung sicher nur für die minimal benötigte Zeit hält und einfacher threadsicher zu machen ist.


Aber das würde UOW aus dem Fenster werfen!
Seabizkit

Ja, aber weniger Deadlock-Wahrscheinlichkeit und minimale Anzahl offener Verbindungen. Theorie gegen Praxis :) Und für Get-Operationen spielt UOW nicht immer eine Rolle.
Dexion

Ich mag diese Methode. Kann jemand bitte erklären, ob diese Methode empfohlen wird, wenn wir DI sprechen.
Dilhan Jayathilake

0

Ich denke, dass der erste Ansatz besser ist, Sie müssen nie einen Datenbankkontext für jedes Repository erstellen, selbst wenn Sie ihn entsorgen. Im ersten Fall können Sie jedoch eine databaseFactory verwenden, um nur einen Datenbankkontext zu instanziieren:

 public class DatabaseFactory : Disposable, IDatabaseFactory {
    private XXDbContext dataContext;

    public ISefeViewerDbContext Get() {
        return dataContext ?? (dataContext = new XXDbContext());
    }

    protected override void DisposeCore() {
        if (dataContext != null) {
            dataContext.Dispose();
        }
    }
}

Und verwenden Sie diese Instanz im Repository:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private IXXDbContext dataContext;

    private readonly DbSet<TEntity> dbset;

    public Repository(IDatabaseFactory databaseFactory) {
        if (databaseFactory == null) {
            throw new ArgumentNullException("databaseFactory", "argument is null");
        }
        DatabaseFactory = databaseFactory;
        dbset = DataContext.Set<TEntity>();
    }

    public ISefeViewerDbContext DataContext {
        get { return (dataContext ?? (dataContext = DatabaseFactory.Get()));
    }

    public virtual TEntity GetById(Guid id){
        return dbset.Find(id);
    }
....
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.