Oddzielanie dostępu do danych w ASP.NET MVC

35

Chcę się upewnić, że przestrzegam standardów branżowych i najlepszych praktyk podczas mojego pierwszego prawdziwego cracku w MVC. W tym przypadku jest to ASP.NET MVC, przy użyciu C #.

Będę używał Entity Framework 4.1 dla mojego modelu z obiektami zawierającymi kod (baza danych już istnieje), więc będzie obiekt DBContext do pobierania danych z bazy danych.

W pokazach, które przeglądałem na stronie asp.net, kontrolery mają w sobie kod dostępu do danych. Nie wydaje mi się to właściwe, szczególnie gdy przestrzegam praktyk DRY (nie powtarzaj się).

Załóżmy na przykład, że piszę aplikację internetową do użytku w bibliotece publicznej i mam kontroler do tworzenia, aktualizowania i usuwania książek w katalogu.

Kilka akcji może wymagać numeru ISBN i trzeba zwrócić obiekt „Book” (pamiętaj, że prawdopodobnie nie jest to w 100% poprawny kod):

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

Zamiast tego, czy rzeczywiście powinienem mieć metodę w obiekcie kontekstu db, aby zwrócić jedną książkę? Wydaje mi się, że jest to dla mnie lepsze oddzielenie i pomaga promować DRY, ponieważ może być konieczne uzyskanie obiektu Book przez ISBN gdzie indziej w mojej aplikacji internetowej.

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

Czy to prawidłowy zestaw zasad, których należy przestrzegać przy kodowaniu mojej aplikacji?

Albo, jak sądzę, bardziej subiektywne pytanie brzmiałoby: „czy to właściwy sposób, aby to zrobić?”

scott.korin
źródło

Odpowiedzi:

55

Ogólnie rzecz biorąc, chcesz, aby Twoi Kontrolery robili tylko kilka rzeczy:

  1. Obsługuj przychodzące żądanie
  2. Deleguj przetwarzanie do jakiegoś obiektu biznesowego
  3. Przekaż wynik przetwarzania biznesowego do odpowiedniego widoku do renderowania

Nie powinno być jakikolwiek dostęp do danych lub złożona logika biznesowa w kontrolerze.

[W najprostszej aplikacji możesz prawdopodobnie uciec się do podstawowych działań CRUD danych w kontrolerze, ale kiedy zaczniesz dodawać więcej niż proste wywołania Get i Update, będziesz chciał podzielić swoje przetwarzanie na osobną klasę. ]

Administratorzy zazwyczaj będą polegać na „usłudze” w celu wykonania rzeczywistej pracy przetwarzania. W swojej klasie usługi, którą mogą pracować bezpośrednio ze źródła danych (w przypadku DbContext), ale po raz kolejny, jeśli znajdziesz się pisząc wiele reguł biznesowych oprócz dostępu do danych, będzie prawdopodobnie chcesz oddzielić biznes logika z dostępu do danych.

W tym momencie prawdopodobnie będziesz mieć klasę, która nie robi nic oprócz dostępu do danych. Czasami nazywa się to Repozytorium, ale tak naprawdę nie ma znaczenia, jak się nazywa. Chodzi o to, że cały kod do pobierania i wyprowadzania danych z bazy danych znajduje się w jednym miejscu.

Dla każdego projektu MVC, nad którym pracowałem, zawsze miałem strukturę taką jak:

Kontroler

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

Usługi biznesowe

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;

     } 
}

Magazyn

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
źródło
+1 Ogólnie świetna rada, choć wątpię, czy abstrakcja repozytorium ma jakąkolwiek wartość.
MattDavey
3
@MattDavey Tak, na samym początku (lub w przypadku najprostszych aplikacji) trudno dostrzec potrzebę warstwy repozytorium, ale gdy tylko masz umiarkowany poziom złożoności logiki biznesowej, staje się oczywiste, że oddzielić dostęp do danych. Nie jest to jednak łatwe do przekazania w prosty sposób.
Eric King
1
@Billy Jądro IoC nie musi znajdować się w projekcie MVC. Możesz mieć go we własnym projekcie, od którego zależy projekt MVC, ale który z kolei zależy od projektu repozytorium. Zasadniczo tego nie robię, ponieważ nie czuję takiej potrzeby. Mimo to, jeśli nie chcesz, aby Twój projekt MVC wywoływał klasy repozytorium ... to nie. Nie jestem wielkim fanem ścięgna ścięgien, abym mógł uchronić się przed możliwością programowania, w które raczej się nie angażuję.
Eric King
2
Używamy dokładnie tego wzorca: kontroler-usługa-repozytorium. Chciałbym dodać, że bardzo przydatne jest, aby warstwa usług / repozytorium pobierała obiekty parametrów (np. GetBooksParameters), a następnie korzystała z metod rozszerzenia w ILibraryService, aby przeprowadzać przemianę parametrów. W ten sposób ILibraryService ma prosty punkt wejścia, który pobiera obiekt, a metoda rozszerzenia może doprowadzić do jak największej wariacji parametru bez konieczności przepisywania interfejsów i klas za każdym razem (np. GetBooksByISBN / Customer / Date / Cokolwiek tylko tworzy obiekt GetBooksParameters i wywołuje metodę usługa). Kombinacja jest świetna.
BlackjacketMack
1
@IsaacKleinman Nie pamiętam, który z wielkich napisał go (Bob Martin?), Ale to podstawowe pytanie: czy chcesz Piekarnik. Piec (pizza) czy Pizza. Piec (piekarnik). A odpowiedź brzmi „to zależy”. Zwykle chcemy zewnętrznej usługi (lub jednostki pracy) manipulującej jednym lub większą liczbą obiektów (lub pizzy!). Ale kto powiedział, że te pojedyncze obiekty nie mają zdolności reagowania na rodzaj piekarnika, w którym są pieczone. Wolę OrderRepository.Save (kolejność) niż Order.Save (). Jednak lubię Order.Validate (), ponieważ zamówienie może wiedzieć, że ma swoją idealną formę. Kontekstowe i osobiste.
BlackjacketMack
2

W ten sposób to robię, chociaż wstrzykuję dostawcy danych jako ogólny interfejs usługi danych, aby móc zamienić implementacje.

O ile mi wiadomo, kontroler ma być miejscem, w którym uzyskujesz dane, wykonujesz dowolne czynności i przekazujesz dane do widoku.

Nathan Craddock
źródło
Tak, czytałem o używaniu „interfejsu usługi” dla dostawcy danych, ponieważ pomaga to w testowaniu jednostkowym.
scott.korin