webentwicklung-frage-antwort-db.com.de

Entity Framework 6 Code First - Ist die Repository-Implementierung eine gute?

Ich bin dabei, ein Entity Framework 6-Design mit einem Repository und einer Arbeitseinheit zu implementieren.

Es gibt so viele Artikel und ich bin mir nicht sicher, was der beste Rat ist: Zum Beispiel gefällt mir das hier implementierte Muster wirklich: aus den im Artikel vorgeschlagenen Gründen hier

Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team) schlägt jedoch vor, dies in einem anderen Artikel zu tun: hier

Ich abonniere Pluralsight und es wird jedes Mal, wenn es in einem Kurs verwendet wird, auf eine etwas andere Weise implementiert, sodass die Auswahl eines Designs schwierig ist.

Einige Leute scheinen vorzuschlagen, dass die Arbeitseinheit bereits von DbContext implementiert wurde, wie in diesem post , daher sollten wir sie überhaupt nicht implementieren müssen.

Mir ist klar, dass diese Art von Frage bereits gestellt wurde und dass dies subjektiv sein kann, aber meine Frage ist direkt:

Ich mag den Ansatz im ersten Artikel (Code Fizzle) und wollte wissen, ob er vielleicht besser zu warten und so einfach zu testen ist wie andere Ansätze und sicherer zu betreiben ist.

Alle anderen Ansichten sind mehr als willkommen.

49
davy

@ Chris Hardie ist richtig, EF implementiert UoW out of the box. Viele Leute übersehen jedoch, dass EF auch ein generisches Repository-Muster implementiert:

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

... und dies ist eine ziemlich gute generische Repository-Implementierung, die in das Tool selbst integriert ist.

Warum sich die Mühe machen, eine Menge anderer Schnittstellen und Eigenschaften zu erstellen, wenn DbContext Ihnen alles bietet, was Sie brauchen? Wenn Sie den DbContext hinter Schnittstellen auf Anwendungsebene abstrahieren und die Befehlsabfragesegregation anwenden möchten, können Sie Folgendes tun:

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

Sie können diese drei Schnittstellen für den gesamten Entitätszugriff verwenden und müssen sich nicht um das Einfügen von drei oder mehr verschiedenen Repositorys in Geschäftscode kümmern, der mit drei oder mehr Entitätssätzen funktioniert. Natürlich würden Sie IoC weiterhin verwenden, um sicherzustellen, dass nur eine DbContext-Instanz pro Webanforderung vorhanden ist, aber alle drei Schnittstellen werden von derselben Klasse implementiert, was die Implementierung vereinfacht.

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

Sie müssen jetzt nur noch eine einzige Schnittstelle in Ihre Abhängigkeit einfügen, unabhängig davon, mit wie vielen verschiedenen Entitäten sie arbeiten muss:

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

Eine meiner Lieblingssachen an diesem Design ist, dass es die Entitätsspeicherabhängigkeiten von Consumer minimiert. In diesem Beispiel ist RecipeController der Consumer, aber in einer realen Anwendung wäre der Consumer ein Befehlshandler. (Bei einem Abfragehandler verwenden Sie normalerweise nur IReadEntities, weil Sie nur Daten zurückgeben und keinen Zustand ändern möchten.) In diesem Beispiel verwenden wir jedoch RecipeController als zu untersuchenden Consumer die Abhängigkeiten Implikationen:

Angenommen, Sie haben eine Reihe von Unit-Tests für die oben genannte Aktion geschrieben. In jedem dieser Komponententests stellen Sie den Controller neu auf und übergeben einen Schein an den Konstruktor. Sagen Sie dann, Ihr Kunde möchte, dass Personen beim Erstellen eines neuen Rezepts ein neues Kochbuch erstellen oder ein vorhandenes Kochbuch ergänzen können.

Bei einem Schnittstellenmuster "Repository pro Entität" oder "Repository pro Aggregat" müssten Sie eine neue Repository-Instanz IRepository<Cookbook> In Ihren Controller-Konstruktor einfügen (oder mithilfe der Antwort von @Chris Hardie Code schreiben, um ein weiteres Repository anzuhängen zur UoW-Instanz). Dies würde alle Ihre anderen Komponententests sofort zum Erliegen bringen, und Sie müssten zurückkehren, um den Konstruktionscode in allen zu ändern, eine weitere Scheininstanz zu übergeben und Ihr Abhängigkeitsarray zu erweitern. Mit den obigen Angaben werden jedoch alle Ihre anderen Komponententests immer noch kompiliert. Sie müssen lediglich zusätzliche Tests schreiben, um die neuen Funktionen des Kochbuchs zu beschreiben.

46
danludwig

Es tut mir (nicht) leid zu sagen, dass der Codefizzle, Dykstas Artikel und die vorherigen Antworten falsch sind. Für die einfache Tatsache, dass sie die EF-Entitäten als Domänen Geschäfts-) Objekte verwenden, ist dies eine große WTF.

Update: Für eine weniger technische Erklärung (in einfachen Worten) lesen Sie --- (Repository Pattern for Dummies

Kurz gesagt, die ANY-Repository-Schnittstelle sollte nicht mit ANY-Persistence-Details (ORM-Details) gekoppelt werden. Die Repo-Oberfläche behandelt NUR Objekte, die für den Rest der App sinnvoll sind (Domäne, möglicherweise Benutzeroberfläche wie in der Präsentation). Eine Menge Leute (mit MS an der Spitze, mit Absichten, die ich vermute) machen den Fehler zu glauben, dass sie ihre EF-Entitäten wiederverwenden können oder Geschäftsobjekte auf ihnen sein können.

Während es kann passieren, ist es ziemlich selten. In der Praxis werden viele Domänenobjekte nach Datenbankregeln entworfen, z. B. nach einer schlechten Modellierung. Das Repository dient dazu, den Rest der App (hauptsächlich die Business-Schicht) von ihrer Persistenzform zu entkoppeln.

Wie entkoppeln Sie es, wenn Ihr Repo EF-Entities (Persistenzdetails) oder deren Methoden IQueryable zurückgeben, eine undichte Abstraktion mit falscher Semantik für diesen Zweck (IQueryable ermöglicht es Ihnen, eine Abfrage zu erstellen, was bedeutet, dass Sie Persistenzdetails kennen müssen den Zweck und die Funktionalität des Projektarchivs zu negieren)?

Ein domin-Objekt sollte nie über Persistenz, EF, Joins usw. Bescheid wissen. Es sollte nicht wissen, welche Datenbank-Engine Sie verwenden oder ob Sie eine verwenden. Dasselbe gilt für den Rest der App, wenn Sie möchten, dass sie von den Persistenzdetails abgekoppelt wird .

Die Repository-Schnittstelle weiß nur, was die höheren Schichten wissen. Dies bedeutet, dass eine generische Domain-Repository-Schnittstelle so aussieht

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

Die Implementierung befindet sich im DAL und verwendet EF, um mit der Datenbank zu arbeiten. Die Implementierung sieht jedoch so aus

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

Sie haben nicht wirklich ein konkretes generisches Repository. Die einzige Verwendung eines konkreten generischen Repository besteht darin, dass JEDES Domänenobjekt in serialisierter Form in einer Schlüsselwert-ähnlichen Tabelle gespeichert wird. Bei einem ORM ist dies nicht der Fall.

Was ist mit Abfragen?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

Das UserData ist das Lese-/Ansichtsmodell, das für die Verwendung des Abfragekontexts geeignet ist.

Sie können EF direkt zum Abfragen in einem Abfrage-Handler verwenden, wenn es Ihnen nichts ausmacht, dass Ihre DAL mit Ansichtsmodellen vertraut ist und Sie in diesem Fall kein Abfrage-Repo benötigen.

Fazit

  • Ihr Geschäftsobjekt sollte nichts über EF-Entitäten wissen.
  • Das Repository verwendet ein ORM , aber es macht das ORM niemals für den Rest der App verfügbar, sodass die Repo-Schnittstelle verwendet wird Nur Domänenobjekte oder Ansichtsmodelle (oder jedes andere App-Kontextobjekt, das kein Persistenzdetail ist)
  • Sie teilen dem Repo nicht mit, wie seine Arbeit erledigen soll, d. H. Verwenden Sie IQueryable NIEMALS mit einer Repo-Schnittstelle
  • Wenn Sie die Datenbank nur auf eine einfachere/coole Art und Weise verwenden möchten und es mit einer einfachen CRUD-App zu tun haben, bei der Sie keine Bedenken haben (seien Sie sich dessen sicher), dann überspringen Sie das Repository alle zusammen, verwende direkt EF für alle Daten. Die App wird eng an EF gekoppelt sein, aber zumindest werden Sie den Mittelsmann abschneiden und es wird absichtlich nicht versehentlich sein.

Beachten Sie, dass eine falsche Verwendung des Repository dessen Verwendung ungültig macht und Ihre App weiterhin eng an die Persistenz (ORM) gekoppelt ist.

Wenn Sie glauben, dass der ORM dazu da ist, Ihre Domänenobjekte magisch zu speichern, ist dies nicht der Fall. Der ORM-Zweck besteht darin, einen OOP) -Speicher über relationalen Tabellen zu simulieren. Er hat alles mit Persistenz und nichts mit Domäne zu tun. Verwenden Sie den ORM daher nicht außerhalb der Persistenz.

42
MikeSW

DbContext wird in der Tat mit dem Muster der Arbeitseinheit erstellt. Es ermöglicht allen Entitäten, denselben Kontext zu verwenden, in dem wir mit ihnen arbeiten. Diese Implementierung ist intern für DbContext.

Es sollte jedoch beachtet werden, dass, wenn Sie zwei DbContext -Objekte instanziieren, keines der Objekte die Entitäten des anderen sieht, die sie jeweils verfolgen. Sie sind voneinander isoliert, was problematisch sein kann.

Wenn ich eine MVC-Anwendung erstelle, möchte ich sicherstellen, dass im Verlauf der Anforderung mein gesamter Datenzugriffscode mit einem einzigen DbContext funktioniert. Um dies zu erreichen, wende ich die Arbeitseinheit als Muster außerhalb von DbContext an.

Hier ist mein Unit of Work-Objekt aus einer Grillrezept-App, die ich baue:

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

Ich hänge alle meine Repositorys, denen alle dasselbe DbContext injiziert wurde, an mein Unit of Work-Objekt an. Solange Repositorys vom Unit of Work-Objekt angefordert werden, können wir sicher sein, dass alle unsere Datenzugriffscodes mit derselben DbContext verwaltet werden - eine großartige Sauce!

Wenn ich dies in einer MVC-App verwenden würde, würde ich sicherstellen, dass die Arbeitseinheit während der gesamten Anforderung verwendet wird, indem ich sie im Controller instanziiere und während der gesamten Aktionen verwende:

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

In unserer Aktion können wir sicher sein, dass alle unsere Datenzugriffscodes den gleichen DbContext verwenden:

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();
4
Mister Epic

Beim Durchsuchen des Internets habe ich Folgendes gefunden http://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/ es ist ein zweiteiliger Artikel über die Nützlichkeit der Repository-Muster von Jon Smith. Der zweite Teil befasst sich mit einer Lösung. Ich hoffe es hilft!

3

Das Repository mit der Implementierung von Arbeitseinheitenmustern ist schlecht, um Ihre Frage zu beantworten.

Der DbContext des Entity-Frameworks wird von Microsoft gemäß dem Unit-of-Work-Pattern implementiert. Das bedeutet, dass der context.SaveChanges Ihre Änderungen auf einmal transaktional speichert.

Das DbSet ist auch eine Implementierung des Repository-Musters. Erstellen Sie keine Repositorys, die Sie nur ausführen können:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

Erstellen Sie eine einzeilige Methode für das, was Sie sowieso innerhalb des Dienstes tun können?

Es gibt keinen Vorteil und niemand ändert EF ORM heutzutage zu einem anderen ORM ...

Sie brauchen diese Freiheit nicht ...

Chris Hardie argumentiert, dass mehrere Kontextobjekte instanziiert werden könnten, aber wenn Sie dies bereits tun, machen Sie es falsch ...

Verwenden Sie einfach ein IOC Tool, das Ihnen gefällt, und richten Sie den MyContext per HTTP-Anfrage ein.

Nimm ninject zum Beispiel:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

Dem Service, der die Geschäftslogik ausführt, wird der Kontext injiziert.

Halt es einfach doof :-)

2
Pascal

Sie sollten "Befehls-/Abfrageobjekte" als Alternative in Betracht ziehen. In diesem Bereich finden Sie eine Reihe interessanter Artikel, aber hier ist ein guter:

https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea/

Wenn Sie eine Transaktion über mehrere DB-Objekte benötigen, verwenden Sie ein Befehlsobjekt pro Befehl, um die Komplexität des UOW-Musters zu vermeiden.

Ein Abfrageobjekt pro Abfrage ist für die meisten Projekte wahrscheinlich nicht erforderlich. Stattdessen können Sie mit einem 'FooQueries'-Objekt beginnen , womit ich meine, Sie können mit einem Repository-Muster für READS beginnen, es aber als "Abfragen" bezeichnen, um genau zu sein, dass dies der Fall ist nicht und sollte keine Einfügungen/Aktualisierungen vornehmen.

Später können Sie vielleicht herausfinden, dass sich das Aufteilen einzelner Abfrageobjekte lohnt. Wenn Sie beispielsweise Autorisierung und Protokollierung hinzufügen möchten, können Sie ein Abfrageobjekt in eine Pipeline einspeisen.

1
Darren

Ich benutze immer zuerst UoW mit EF-Code. Ich finde es performanter und einfacher, Ihre Kontexte zu verwalten, um Speicherlecks und dergleichen vorzubeugen. Ein Beispiel für meine Problemumgehung finden Sie auf meinem Github: http://www.github.com/stefchri im RADAR-Projekt.

Wenn Sie Fragen dazu haben, können Sie diese gerne stellen.

0
Gecko IT