Séparer l'accès aux données dans ASP.NET MVC

35

Je veux m'assurer de suivre les normes de l'industrie et les meilleures pratiques avec mon premier vrai crack chez MVC. Dans ce cas, c’est ASP.NET MVC, en utilisant C #.

J'utiliserai Entity Framework 4.1 pour mon modèle, avec des objets commençant par le code (la base de données existe déjà). Il y aura donc un objet DBContext pour extraire des données de la base de données.

Dans les démos que j'ai visionnées sur le site Web asp.net, les contrôleurs ont un code d'accès aux données. Cela ne me semble pas correct, surtout lorsque vous suivez les pratiques de DRY (ne vous répétez pas).

Par exemple, disons que je suis en train d’écrire une application Web à utiliser dans une bibliothèque publique et que j’ai un contrôleur pour créer, mettre à jour et supprimer des livres dans un catalogue.

Plusieurs actions peuvent prendre un ISBN et doivent vouloir renvoyer un objet "Livre" (notez que ce n'est probablement pas du code valide à 100%):

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

Au lieu de cela, dois - je avoir en fait une méthode dans mon db objet de contexte pour retourner un livre? Cela semble être une meilleure séparation pour moi et contribue à promouvoir DRY, car je pourrais avoir besoin d'obtenir un objet Book par ISBN quelque part ailleurs dans mon application Web.

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

Est-ce un ensemble de règles valide à suivre dans le codage de mon application?

Ou, je suppose une question plus subjective serait: "est-ce la bonne façon de le faire?"

scott.korin
la source

Réponses:

55

Généralement, vous voulez que vos contrôleurs ne fassent que quelques choses:

  1. Traiter la demande entrante
  2. Déléguer le traitement à un objet métier
  3. Transmettre le résultat du traitement de l'activité à la vue appropriée pour le rendu

Il ne devrait y avoir aucun accès aux données ou logique métier complexe dans le contrôleur.

[Dans la plus simple des applications, vous pouvez probablement vous passer des actions CRUD de données de base dans votre contrôleur, mais une fois que vous avez commencé à ajouter plus que de simples appels Get et Update, vous allez vouloir répartir votre traitement dans une classe séparée. ]

Vos contrôleurs dépendent généralement d'un "service" pour effectuer le traitement. Dans votre classe de service, vous pouvez travailler directement avec votre source de données (dans votre cas, DbContext), mais encore une fois, si vous vous trouvez en train d'écrire de nombreuses règles métier en plus de l'accès aux données, vous voudrez probablement séparer votre entreprise. logique de votre accès aux données.

À ce stade, vous aurez probablement une classe qui ne fait que l’accès aux données. Parfois, cela s'appelle un référentiel, mais peu importe le nom. Le fait est que tout le code permettant d’obtenir des données dans la base de données se trouve au même endroit.

Pour chaque projet MVC sur lequel j'ai travaillé, je me suis toujours retrouvé avec une structure telle que:

Manette

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

Service d'affaires

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;

     } 
}

Dépôt

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
la source
+1 Dans l'ensemble, c'est un bon conseil, même si je me demandais si l'abstraction du référentiel offrait une valeur.
MattDavey
3
@MattDavey Ouais, au tout début (ou pour les applications les plus simples), il est difficile de voir la nécessité d'une couche de référentiel, mais dès que vous avez un niveau de complexité même modéré dans votre logique métier, il devient évident séparer l'accès aux données. Ce n’est pas facile à exprimer de manière simple.
Eric King
1
@Billy Le noyau IoC n'a pas besoin d'être dans le projet MVC. Vous pourriez l'avoir dans un projet propre, dont dépend le projet MVC, mais qui dépend à son tour du projet de référentiel. Je ne fais généralement pas cela parce que je n'en ressens pas le besoin. Néanmoins, si vous ne voulez pas que votre projet MVC appelle vos classes de référentiel, alors ne le faites pas. Je ne suis pas un grand fan de moi - même jarrets pour que je puisse me protéger contre la possibilité de pratiques de programmation Je ne suis pas susceptible d'engager.
Eric Roi
2
Nous utilisons exactement ce modèle: Controller-Service-Repository. J'aimerais ajouter qu'il est très utile pour nous que le niveau service / référentiel prenne des objets de paramètres (par exemple, GetBooksParameters), puis utilise des méthodes d'extension sur ILibraryService pour gérer les paramètres. Ainsi, ILibraryService a un point d’entrée simple qui prend un objet, et la méthode d’extension peut être aussi folle que possible sans avoir à réécrire les interfaces et les classes à chaque fois (par exemple, GetBooksByISBN / Customer / Date / Quel que soit simplement l’objet un service). Le combo est génial.
BlackjacketMack
1
@ IsacKleinman Je ne me souviens plus lequel des grands l'a écrit (Bob Martin?), Mais c'est une question fondamentale: voulez-vous Oven.Bake (pizza) ou Pizza.Bake (four). Et la réponse est "ça dépend". Habituellement, nous voulons un service extérieur (ou une unité de travail) manipulant un ou plusieurs objets (ou pizzas!). Mais qui peut dire que ces objets individuels n'ont pas la capacité de réagir au type de four dans lequel ils sont cuits? Je préfère OrderRepository.Save (order) à Order.Save (). Cependant, j'aime bien Order.Validate () car l'ordre peut connaître sa propre forme idéale. Contextuel et personnel.
BlackjacketMack
2

C’est ce que je fais, même si j’injecte le fournisseur de données en tant qu’interface de service de données générique afin de pouvoir permuter les implémentations.

Autant que je sache, le contrôleur est censé être l'endroit où vous obtenez les données, effectuez toutes les actions et transmettez les données à la vue.

Nathan Craddock
la source
Oui, j'ai lu des informations sur l'utilisation d'une "interface de service" pour le fournisseur de données, car elle facilite les tests unitaires.
scott.korin