Separazione dell'accesso ai dati in ASP.NET MVC


35

Voglio assicurarmi di seguire gli standard del settore e le migliori pratiche con il mio primo vero crack a MVC. In questo caso, è ASP.NET MVC, usando C #.

Userò Entity Framework 4.1 per il mio modello, con oggetti code-first (il database esiste già), quindi ci sarà un oggetto DBContext per il recupero dei dati dal database.

Nelle demo che ho visto sul sito Web asp.net, i controller hanno al loro interno un codice di accesso ai dati. Questo non mi sembra giusto, specialmente quando segui le pratiche DRY (non ripeterti).

Ad esempio, supponiamo che sto scrivendo un'applicazione Web da utilizzare in una biblioteca pubblica e che disponga di un controller per la creazione, l'aggiornamento e l'eliminazione di libri in un catalogo.

Molte delle azioni potrebbero richiedere un codice ISBN e potrebbero voler restituire un oggetto "Libro" (si noti che probabilmente non è un codice valido al 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);
    }
}

Invece, dovrei effettivamente avere un metodo nel mio oggetto di contesto db per restituire un libro? Sembra che sia una migliore separazione per me e aiuta a promuovere il DRY, perché potrei aver bisogno di ottenere un oggetto Book tramite ISBN da qualche altra parte nella mia applicazione 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));
    }
}

È un insieme valido di regole da seguire nella codifica della mia applicazione?

Oppure, suppongo che una domanda più soggettiva sarebbe: "è questo il modo giusto di farlo?"

Risposte:


55

In genere, vuoi che i tuoi controller facciano solo alcune cose:

  1. Gestire la richiesta in arrivo
  2. Delegare l'elaborazione ad alcuni oggetti business
  3. Passare il risultato dell'elaborazione aziendale alla vista appropriata per il rendering

Non ci dovrebbe essere alcun accesso ai dati o complesse logiche aziendali nel controller.

[Nelle app più semplici, probabilmente puoi cavartela con le azioni CRUD di dati di base nel tuo controller, ma una volta che inizi ad aggiungere più di semplici chiamate Get e Update, vorrai suddividere l'elaborazione in una classe separata. ]

I controller dipenderanno generalmente da un "Servizio" per eseguire il lavoro di elaborazione effettivo. Nella tua classe di servizio si può lavorare direttamente con l'origine dati (nel tuo caso, il DbContext), ma ancora una volta, se vi trovate a scrivere un sacco di regole di business, oltre alla accesso ai dati, probabilmente si vuole separare il vostro business logica dal tuo accesso ai dati.

A quel punto, probabilmente avrai una classe che non fa altro che l'accesso ai dati. A volte questo si chiama Repository, ma non importa quale sia il nome. Il punto è che tutto il codice per ottenere i dati dentro e fuori dal database è in un posto.

Per ogni progetto MVC a cui ho lavorato, ho sempre finito con una struttura come:

controllore

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

Servizio per affari

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;

     } 
}

deposito

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
}

+1 Nel complesso un ottimo consiglio, anche se mi chiederei se l'astrazione del repository fornisse un valore.
MattDavey,

3
@MattDavey Sì, all'inizio (o per la più semplice delle app) è difficile vedere la necessità di un livello di repository, ma non appena hai anche un livello moderato di complessità nella tua logica aziendale diventa un gioco da ragazzi separare l'accesso ai dati. Tuttavia, non è facile trasmetterlo in modo semplice.
Eric King

1
@Billy Il kernel IoC non deve essere nel progetto MVC. Potresti averlo in un progetto a parte, dal quale dipende il progetto MVC, ma che a sua volta dipende dal progetto di repository. In genere non lo faccio perché non ne sento il bisogno. Anche così, se non vuoi che il tuo progetto MVC chiami le tue classi di repository ... allora non farlo. Non sono un grande fan del tendine del ginocchio, così da potermi proteggere dalla possibilità di pratiche di programmazione che probabilmente non mi impegnerò.
Eric King,

2
Usiamo esattamente questo schema: Controller-Service-Repository. Vorrei aggiungere che è molto utile per noi che il livello servizio / repository prenda oggetti parametri (ad esempio GetBooksParameters) e quindi usi i metodi di estensione su ILibraryService per fare il wrangling dei parametri. In questo modo ILibraryService ha un semplice punto di ingresso che prende un oggetto e il metodo di estensione può diventare il più folle possibile senza dover riscrivere interfacce e classi ogni volta (ad esempio GetBooksByISBN / Customer / Date / qualunque cosa formi semplicemente l'oggetto GetBooksParameters e chiama il servizio). La combinazione è stata fantastica.
Blackjacket:

1
@IsaacKleinman Non ricordo quale dei grandi lo scrisse (Bob Martin?) Ma è una domanda fondamentale: vuoi Oven.Bake (pizza) o Pizza.Bake (forno). E la risposta è "dipende". Di solito vogliamo un servizio esterno (o unità di lavoro) che manipoli uno o più oggetti (o pizze!). Ma chi può dire che quei singoli oggetti non hanno la capacità di reagire al tipo di forno in cui vengono cotti. Preferisco OrderRepository.Save (order) a Order.Save (). Tuttavia, mi piace Order.Validate () perché l'ordine può sapere che è il proprio modulo ideale. Contestuale e personale.
Blackjacket:

2

Questo è il modo in cui l'ho fatto, anche se sto iniettando il fornitore di dati come interfaccia di servizio dati generico in modo da poter scambiare le implementazioni.

Per quanto ne so, il controller è destinato a essere dove si ottengono i dati, eseguire qualsiasi azione e passare i dati alla vista.


Sì, ho letto sull'utilizzo di una "interfaccia di servizio" per il fornitore di dati, perché aiuta con i test delle unità.
scott.korin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.