Datenzugriff in ASP.NET MVC trennen

35

Ich möchte sicherstellen, dass ich bei meinem ersten echten Sprung bei MVC Branchenstandards und Best Practices befolge. In diesem Fall ist es ASP.NET MVC mit C #.

Ich werde Entity Framework 4.1 für mein Modell mit Code-First-Objekten verwenden (die Datenbank ist bereits vorhanden), sodass es ein DBContext-Objekt zum Abrufen von Daten aus der Datenbank gibt.

In den Demos, die ich auf der asp.net-Website durchlaufen habe, haben Controller einen Datenzugriffscode. Dies scheint mir nicht richtig zu sein, besonders wenn man den DRY-Praktiken folgt (sich nicht wiederholen).

Angenommen, ich schreibe eine Webanwendung, die in einer öffentlichen Bibliothek verwendet werden soll, und ich verfüge über einen Controller zum Erstellen, Aktualisieren und Löschen von Büchern in einem Katalog.

Einige der Aktionen benötigen möglicherweise eine ISBN und müssen ein "Book" -Objekt zurückgeben (beachten Sie, dass dies wahrscheinlich kein 100% gültiger Code ist):

public class BookController : Controller
{
    LibraryDBContext _db = new LibraryDBContext();

    public ActionResult Details(String ISBNtoGet)
    {
        Book currentBook = _db.Books.Single(b => b.ISBN == ISBNtoGet);
        return View(currentBook);
    }

    public ActionResult Edit(String ISBNtoGet)
    {
        Book currentBook = _db.Books.Single(b => b.ISBN == ISBNtoGet);
        return View(currentBook);
    }
}

Stattdessen sollte eigentlich habe ich eine Methode in meinem db Kontextobjekt ein Buch zurückzubringen? Das scheint mir eine bessere Trennung zu sein und hilft, DRY zu fördern, da ich in meiner Webanwendung möglicherweise ein Buchobjekt über die ISBN an einer anderen Stelle abrufen muss.

public partial class LibraryDBContext: DBContext
{
    public Book GetBookByISBN(String ISBNtoGet)
    {
        return Books.Single(b => b.ISBN == ISBNtoGet);
    }
}

public class BookController : Controller
{
    LibraryDBContext _db = new LibraryDBContext();

    public ActionResult Details(String ISBNtoGet)
    {
        return View(_db.GetBookByISBN(ISBNtoGet));
    }

    public ActionResult Edit(ByVal ISBNtoGet as String)
    {
        return View(_db.GetBookByISBN(ISBNtoGet));
    }
}

Ist dies ein gültiger Satz von Regeln, die bei der Codierung meiner Anwendung zu beachten sind?

Oder ich denke, eine subjektivere Frage wäre: "Ist dies der richtige Weg, um es zu tun?"

scott.korin
quelle

Antworten:

55

Im Allgemeinen möchten Sie, dass Ihre Controller nur ein paar Dinge tun:

  1. Behandeln Sie die eingehende Anforderung
  2. Delegieren Sie die Verarbeitung an ein Geschäftsobjekt
  3. Übergeben Sie das Ergebnis der Geschäftsverarbeitung an die entsprechende Ansicht zum Rendern

Der Controller sollte keinen Datenzugriff oder komplexe Geschäftslogik enthalten.

[In den einfachsten Apps können Sie wahrscheinlich mit CRUD-Aktionen für Basisdaten in Ihrem Controller davonkommen. Wenn Sie jedoch mehr als nur Get- und Update-Aufrufe hinzufügen, möchten Sie Ihre Verarbeitung in eine separate Klasse aufteilen. ]

Ihre Controller sind normalerweise auf einen 'Service' angewiesen, um die eigentliche Verarbeitungsarbeit zu erledigen. In Ihrer Serviceklasse arbeiten Sie möglicherweise direkt mit Ihrer Datenquelle (in Ihrem Fall dem DbContext). Wenn Sie jedoch zusätzlich zum Datenzugriff viele Geschäftsregeln schreiben, möchten Sie wahrscheinlich Ihr Unternehmen trennen Logik aus Ihrem Datenzugriff.

Zu diesem Zeitpunkt haben Sie wahrscheinlich eine Klasse, die nur den Datenzugriff ausführt. Manchmal wird dies als Repository bezeichnet, aber es spielt keine Rolle, wie der Name lautet. Der Punkt ist, dass sich der gesamte Code zum Ein- und Auslesen von Daten aus der Datenbank an einem Ort befindet.

Für jedes MVC-Projekt, an dem ich gearbeitet habe, hatte ich immer eine Struktur wie:

Regler

public class BookController : Controller
{
    ILibraryService _libraryService;

    public BookController(ILibraryService libraryService)
    {
        _libraryService = libraryService;
    }

    public ActionResult Details(String isbn)
    {
        Book currentBook = _libraryService.RetrieveBookByISBN(isbn);
        return View(ConvertToBookViewModel(currentBook));
    }

    public ActionResult DoSomethingComplexWithBook(ComplexBookActionRequest request)
    {
        var responseViewModel = _libraryService.ProcessTheComplexStuff(request);
        return View(responseViewModel);
    }
}

Unternehmensdienstleistungen

public class LibraryService : ILibraryService
{
     IBookRepository _bookRepository;
     ICustomerRepository _customerRepository;

     public LibraryService(IBookRepository bookRepository, 
                           ICustomerRepository _customerRepository )
     {
          _bookRepository = bookRepository;
          _customerRepository = customerRepository;
     }

     public Book RetrieveBookByISBN(string isbn)
     {
          return _bookRepository.GetBookByISBN(isbn);
     }

     public ComplexBookActionResult ProcessTheComplexStuff(ComplexBookActionRequest request)
     {
          // Possibly some business logic here

          Book book = _bookRepository.GetBookByISBN(request.Isbn);
          Customer customer = _customerRepository.GetCustomerById(request.CustomerId);

          // Probably more business logic here

          _libraryRepository.Save(book);

          return complexBusinessActionResult;

     } 
}

Repository

public class BookRepository : IBookRepository
{
     LibraryDBContext _db = new LibraryDBContext();

     public Book GetBookByIsbn(string isbn)
     {
         return _db.Books.Single(b => b.ISBN == isbn);
     }

     // And the rest of the data access
}
Eric King
quelle
+1 Insgesamt ein guter Rat, obwohl ich die Frage stellen würde, ob die Repository-Abstraktion einen Wert liefert.
MattDavey
3
@MattDavey Ja, am Anfang (oder für die einfachsten Apps) ist die Notwendigkeit einer Repository-Ebene schwer einzusehen, aber sobald Sie in Ihrer Geschäftslogik ein moderates Maß an Komplexität haben, wird dies zum Kinderspiel trennen Sie den Datenzugriff. Es ist jedoch nicht einfach, das auf einfache Weise zu vermitteln.
Eric King
1
@Billy Der IoC - Kernel nicht hat , um in dem MVC - Projekt. Sie können es in einem eigenen Projekt haben, von dem das MVC-Projekt abhängt, das wiederum vom Repository-Projekt abhängt. Ich mache das im Allgemeinen nicht, weil ich nicht das Bedürfnis dazu habe. Wenn Sie jedoch nicht möchten, dass Ihr MVC-Projekt Ihre Repository-Klassen aufruft, tun Sie dies nicht. Ich bin kein großer Fan von Kniesehnen, um mich vor möglichen Programmierpraktiken zu schützen, mit denen ich mich wahrscheinlich nicht beschäftigen werde.
Eric King
2
Wir verwenden genau dieses Muster: Controller-Service-Repository. Ich möchte hinzufügen, dass es für uns sehr nützlich ist, dass die Service- / Repository-Ebene Parameterobjekte (z. B. GetBooksParameters) übernimmt und dann Erweiterungsmethoden für ILibraryService verwendet, um Parameter-Wrangling durchzuführen. Auf diese Weise hat ILibraryService einen einfachen Einstiegspunkt, der ein Objekt nimmt, und die Erweiterungsmethode kann so parametrisch wie möglich werden, ohne dass Schnittstellen und Klassen jedes Mal neu geschrieben werden müssen (z. B. GetBooksByISBN / Customer / Date / Whatever bildet nur das GetBooksParameters-Objekt und ruft das auf Bedienung). Die Combo war großartig.
BlackjacketMack
1
@IsaacKleinman Ich kann mich nicht erinnern, welcher der Großen es geschrieben hat (Bob Martin?), Aber es ist eine grundlegende Frage: Willst du Oven.Bake (Pizza) oder Pizza.Bake (Ofen). Und die Antwort ist "es kommt darauf an". Normalerweise möchten wir, dass ein externer Dienst (oder eine Arbeitseinheit) ein oder mehrere Objekte (oder Pizzen!) Manipuliert. Aber wer soll sagen, dass diese einzelnen Objekte nicht auf die Art des Ofens reagieren können, in dem sie gebacken werden? Ich bevorzuge OrderRepository.Save (order) gegenüber Order.Save (). Ich mag jedoch Order.Validate (), weil die Bestellung wissen kann, dass es sich um eine eigene ideale Form handelt. Kontextuell und persönlich.
BlackjacketMack
2

So habe ich es gemacht, obwohl ich den Datenprovider als generische Datendienstschnittstelle einspeise, damit ich Implementierungen austauschen kann.

Soweit ich weiß, ist der Controller dafür vorgesehen, Daten abzurufen, Aktionen auszuführen und Daten an die Ansicht zu übergeben.

Nathan Craddock
quelle
Ja, ich habe gelesen, dass für den Datenprovider eine "Service-Schnittstelle" verwendet werden soll, da diese beim Testen von Einheiten hilfreich ist.
scott.korin