Separar el acceso a datos en ASP.NET MVC

35

Quiero asegurarme de que sigo los estándares y las mejores prácticas de la industria con mi primer crack real en MVC. En este caso, es ASP.NET MVC, que usa C #.

Usaré Entity Framework 4.1 para mi modelo, con objetos de código primero (la base de datos ya existe), por lo que habrá un objeto DBContext para recuperar datos de la base de datos.

En las demostraciones que revisé en el sitio web asp.net, los controladores tienen código de acceso a datos. Esto no me parece correcto, especialmente cuando sigo las prácticas DRY (no se repita).

Por ejemplo, supongamos que estoy escribiendo una aplicación web para usar en una biblioteca pública, y tengo un controlador para crear, actualizar y eliminar libros en un catálogo.

Varias de las acciones pueden tomar un ISBN y deben querer devolver un objeto "Libro" (tenga en cuenta que probablemente este no sea un código 100% válido):

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

En cambio, ¿ debería tener un método en mi objeto de contexto db para devolver un Libro? Parece que es una mejor separación para mí y ayuda a promover DRY, porque podría necesitar obtener un objeto Book por ISBN en algún otro lugar de mi aplicación 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));
    }
}

¿Es este un conjunto válido de reglas a seguir en la codificación de mi aplicación?

O supongo que una pregunta más subjetiva sería: "¿es esta la forma correcta de hacerlo?"

scott.korin
fuente

Respuestas:

55

En general, desea que sus controladores solo hagan algunas cosas:

  1. Manejar la solicitud entrante
  2. Delegar el procesamiento a algún objeto comercial
  3. Pase el resultado del procesamiento comercial a la vista adecuada para la representación

No debería haber ningún acceso a datos o lógica empresarial compleja en el controlador.

[En la aplicación más simple, probablemente pueda salirse con la suya con acciones CRUD de datos básicos en su controlador, pero una vez que comience a agregar más que simples llamadas Get and Update, querrá dividir su procesamiento en una clase separada. ]

Sus controladores generalmente dependerán de un 'Servicio' para realizar el trabajo de procesamiento real. En su clase de servicio, puede trabajar directamente con su fuente de datos (en su caso, el DbContext), pero una vez más, si se encuentra escribiendo muchas reglas de negocios además del acceso a datos, probablemente quiera separar su negocio lógica de su acceso a datos.

En ese punto, probablemente tendrá una clase que no hace nada más que el acceso a los datos. A veces esto se llama Repositorio, pero en realidad no importa cuál sea el nombre. El punto es que todo el código para ingresar y sacar datos de la base de datos está en un solo lugar.

Para cada proyecto MVC en el que he trabajado, siempre terminé con una estructura como:

Controlador

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

Servicio empresarial

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;

     } 
}

Repositorio

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
fuente
+1 En general, un gran consejo, aunque me preguntaría si la abstracción del repositorio estaba proporcionando algún valor.
MattDavey
3
@MattDavey Sí, al principio (o para las aplicaciones más simples) es difícil ver la necesidad de una capa de repositorio, pero tan pronto como tenga un nivel moderado de complejidad en la lógica de su negocio se vuelve obvio Separar el acceso a los datos. Sin embargo, no es fácil transmitir eso de una manera simple.
Eric King
1
@Billy El núcleo IoC no tiene que estar en el proyecto MVC. Podría tenerlo en un proyecto propio, del que depende el proyecto MVC, pero que a su vez depende del proyecto de repositorio. Generalmente no hago eso porque no siento la necesidad de hacerlo. Aun así, si no desea que su proyecto MVC llame a sus clases de repositorio ... entonces no lo haga. No soy un gran fan de la misma manera que paraliza las puedo protegerme de la posibilidad de prácticas de programación No estoy propensos a participar en.
Eric Rey
2
Utilizamos exactamente este patrón: Controller-Service-Repository. Quisiera agregar que es muy útil para nosotros que el nivel de servicio / repositorio tome objetos de parámetros (por ejemplo, GetBooksParameters) y luego use métodos de extensión en ILibraryService para realizar la distribución de parámetros. De esa manera, ILibraryService tiene un punto de entrada simple que toma un objeto, y el método de extensión puede volverse lo más loco posible sin tener que volver a escribir interfaces y clases cada vez (por ejemplo, GetBooksByISBN / Customer / Date / Whatever / simplemente forma el objeto GetBooksParameters y llama al Servicio). El combo ha sido genial.
BlackjacketMack
1
@IsaacKleinman No recuerdo cuál de los grandes lo escribió (Bob Martin?) Pero es una pregunta fundamental: ¿quieres Oven.Bake (pizza) o Pizza.Bake (horno)? Y la respuesta es 'depende'. Por lo general, queremos un servicio externo (o unidad de trabajo) que manipule uno o más objetos (¡o pizzas!). Pero quién puede decir que esos objetos individuales no tienen la capacidad de reaccionar al tipo de horno en el que se están horneando. Prefiero OrderRepository.Save (order) a Order.Save (). Sin embargo, me gusta Order.Validate () porque el pedido puede conocer su propia forma ideal. Contextual y personal.
BlackjacketMack
2

Esta es la forma en que lo he estado haciendo, aunque estoy inyectando el proveedor de datos como una interfaz de servicio de datos genérica para poder intercambiar implementaciones.

Hasta donde yo sé, el controlador está destinado a ser donde obtienes datos, realizas cualquier acción y pasas datos a la vista.

Nathan Craddock
fuente
Sí, he leído sobre el uso de una "interfaz de servicio" para el proveedor de datos, porque ayuda con las pruebas unitarias.
scott.korin