Separando o acesso a dados no ASP.NET MVC

35

Quero ter certeza de que estou seguindo os padrões e as melhores práticas do setor com meu primeiro crack real no MVC. Nesse caso, é o ASP.NET MVC, usando C #.

Eu usarei o Entity Framework 4.1 para o meu modelo, com objetos de primeiro código (o banco de dados já existe), portanto haverá um objeto DBContext para recuperar dados do banco de dados.

Nas demos que eu já fiz no site asp.net, os controladores têm um código de acesso a dados. Isso não parece certo para mim, especialmente ao seguir as práticas de DRY (não se repita).

Por exemplo, digamos que estou escrevendo um aplicativo Web para ser usado em uma biblioteca pública e que possuo um controlador para criar, atualizar e excluir livros em um catálogo.

Várias das ações podem ter um ISBN e precisam retornar um objeto "Livro" (observe que esse código provavelmente não é 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);
    }
}

Em vez disso, eu deveria realmente ter um método no meu objeto de contexto db para retornar um livro? Parece que é uma separação melhor para mim e ajuda a promover o DRY, porque talvez eu precise obter um objeto Book pelo ISBN em outro lugar do meu aplicativo da 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));
    }
}

Esse é um conjunto válido de regras a serem seguidas na codificação do meu aplicativo?

Ou, acho que uma pergunta mais subjetiva seria: "esse é o caminho certo para fazê-lo?"

scott.korin
fonte

Respostas:

55

Geralmente, você deseja que seus controladores façam apenas algumas coisas:

  1. Manipular a solicitação recebida
  2. Delegar o processamento para algum objeto de negócios
  3. Passe o resultado do processamento de negócios para a visualização apropriada para renderização

Não deve haver nenhum acesso a dados ou lógica comercial complexa no controlador.

[No mais simples dos aplicativos, você provavelmente pode se safar das ações básicas de CRUD de dados em seu controlador, mas assim que começar a adicionar mais do que simples chamadas Get e Update, você desejará dividir seu processamento em uma classe separada. ]

Seus controladores geralmente dependem de um 'Serviço' para realizar o trabalho de processamento real. Na sua classe de serviço, você pode trabalhar diretamente com sua fonte de dados (no seu caso, o DbContext), mas mais uma vez, se você estiver escrevendo muitas regras de negócios além do acesso a dados, provavelmente desejará separar seus negócios. lógica do seu acesso a dados.

Nesse ponto, você provavelmente terá uma classe que não faz nada além do acesso aos dados. Às vezes, isso é chamado de repositório, mas realmente não importa qual é o nome. O ponto é que todo o código para entrada e saída de dados do banco de dados está em um só lugar.

Para cada projeto MVC em que trabalhei, sempre acabei com uma estrutura 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);
    }
}

Serviço comercial

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;

     } 
}

Repositório

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
fonte
Um ótimo conselho geral, embora eu questionasse se a abstração do repositório estava fornecendo algum valor.
precisa saber é o seguinte
3
@MattDavey Sim, no início (ou para os aplicativos mais simples) é difícil perceber a necessidade de uma camada de repositório, mas, assim que você tem um nível moderado de complexidade em sua lógica de negócios, torna-se um acéfalo para separar o acesso a dados. Não é fácil transmitir isso de uma maneira simples, no entanto.
Eric Rei
11
@ Billy O kernel IoC não precisa estar no projeto MVC. Você poderia tê-lo em um projeto próprio, do qual o projeto MVC depende, mas que por sua vez depende do projeto do repositório. Geralmente não faço isso porque não sinto necessidade. Mesmo assim, se você não deseja que seu projeto MVC chame suas classes de repositório ... então não. Eu não sou um grande fã de me atrapalhar, para poder me proteger da possibilidade de práticas de programação nas quais provavelmente não me engajarei.
Eric King
2
Usamos exatamente esse padrão: Controller-Service-Repository. Eu gostaria de acrescentar que é muito útil fazer com que a camada de serviço / repositório obtenha objetos de parâmetros (por exemplo, GetBooksParameters) e, em seguida, use métodos de extensão no ILibraryService para fazer a troca de parâmetros. Dessa forma, o ILibraryService possui um ponto de entrada simples que pega um objeto, e o método de extensão pode ficar o mais louco possível possível, sem precisar reescrever interfaces e classes todas as vezes (por exemplo, GetBooksByISBN / Customer / Date / Whatever, que forma o objeto GetBooksParameters e chama o serviço). A combinação tem sido ótima.
amigos estão
11
@IsaacKleinman Não me lembro qual dos grandes escreveu (Bob Martin?), Mas é uma pergunta fundamental: você quer Oven.Bake (pizza) ou Pizza.Bake (forno). E a resposta é 'depende'. Normalmente, queremos um serviço externo (ou unidade de trabalho) manipulando um ou mais objetos (ou pizzas!). Mas quem pode dizer que esses objetos individuais não têm capacidade de reagir ao tipo de forno em que estão sendo assados. Prefiro OrderRepository.Save (order) a Order.Save (). No entanto, eu gosto de Order.Validate () porque o pedido pode saber sua própria forma ideal. Contextual e pessoal.
BlackjacketMack
2

É dessa maneira que eu faço isso, embora esteja injetando o provedor de dados como uma interface genérica de serviço de dados, para que eu possa trocar implementações.

Até onde eu sei, o controlador deve estar onde você obtém dados, executa quaisquer ações e passa dados para a visualização.

Nathan Craddock
fonte
Sim, eu li sobre o uso de uma "interface de serviço" para o provedor de dados, porque ajuda no teste de unidade.
22412 scott.korin