Разделение доступа к данным в ASP.NET MVC

35

Я хочу удостовериться, что я следую отраслевым стандартам и передовым практикам с моей первой настоящей разработкой в ​​MVC. В данном случае это ASP.NET MVC, использующий C #.

Я буду использовать Entity Framework 4.1 для моей модели с объектами первого кода (база данных уже существует), поэтому будет объект DBContext для извлечения данных из базы данных.

В демонстрациях, которые я просмотрел на сайте asp.net, контроллеры содержат код доступа к данным. Это не кажется мне правильным, особенно когда следуешь СУХИМ (не повторяйся) практикам.

Например, скажем, я пишу веб-приложение для использования в публичной библиотеке, и у меня есть контроллер для создания, обновления и удаления книг в каталоге.

Некоторые из действий могут потребовать ISBN и могут потребовать возврата объекта «Книга» (обратите внимание, что это, вероятно, не на 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);
    }
}

Вместо этого, должен ли я на самом деле иметь метод в моем объекте контекста БД для возврата одной Книги? Кажется, что это лучшее разделение для меня и помогает продвигать DRY, потому что мне может понадобиться получить объект Book по ISBN где-то еще в моем веб-приложении.

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

Это действительный набор правил, которым нужно следовать в кодировке моего приложения?

Или, я думаю, более субъективный вопрос был бы: «Это правильный способ сделать это?»

scott.korin
источник

Ответы:

55

Как правило, вы хотите, чтобы ваши контроллеры делали только несколько вещей:

  1. Обработать входящий запрос
  2. Делегировать обработку некоторому бизнес-объекту
  3. Передать результат бизнес-обработки в соответствующее представление для рендеринга.

Там не должно быть никаких доступ к данным или сложной бизнес - логики в контроллере.

[В простейших приложениях вы, вероятно, можете отказаться от базовых действий CRUD с данными в вашем контроллере, но как только вы начнете добавлять более простые вызовы Get и Update, вы захотите разделить обработку на отдельный класс. ]

Ваши контроллеры обычно будут зависеть от «Сервиса» для выполнения фактической работы по обработке. В вашем классе обслуживания вы можете работать непосредственно с источником данных (в вашем случае, DbContext), но, опять же, если вы обнаружите, что помимо бизнес-данных вы пишете много бизнес-правил, вы, вероятно, захотите отделить свой бизнес логика от вашего доступа к данным.

В этот момент у вас, вероятно, будет класс, который ничего не делает, кроме доступа к данным. Иногда это называется хранилищем, но на самом деле не имеет значения, как оно называется. Дело в том, что весь код для получения данных в базу данных и из нее находится в одном месте.

Для каждого проекта MVC, над которым я работал, у меня всегда была такая структура:

контроллер

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

Бизнес Сервис

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;

     } 
}

вместилище

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 В целом отличный совет, хотя я бы усомнился, представляла ли абстракция хранилища какую-либо ценность.
MattDavey
3
@MattDavey Да, в самом начале (или для самых простых приложений) трудно увидеть потребность в уровне хранилища, но как только у вас есть даже умеренный уровень сложности в вашей бизнес-логике, это становится легкой задачей отделить доступ к данным. Хотя это не так просто передать.
Эрик Кинг,
1
@Billy Ядро IoC не обязательно должно быть в проекте MVC. Вы можете иметь его в своем собственном проекте, от которого зависит проект MVC, но который в свою очередь зависит от проекта репозитория. Обычно я этого не делаю, потому что не чувствую в этом необходимости. Тем не менее, если вы не хотите, чтобы ваш проект MVC вызывал ваши классы репозитория ... то не делайте этого. Я не большой поклонник того, чтобы ограничивать себя, чтобы я мог защитить себя от возможности практики программирования, которой я вряд ли буду заниматься.
Эрик Кинг,
2
Мы используем именно этот шаблон: Controller-Service-Repository. Я хотел бы добавить, что для нас очень полезно, чтобы уровень обслуживания / репозитория принимал объекты параметров (например, GetBooksParameters), а затем использовал методы расширения в ILibraryService, чтобы выполнять преобразование параметров. Таким образом, ILibraryService имеет простую точку входа, которая принимает объект, и метод расширения может сводиться к нулю как параметр без необходимости каждый раз переписывать интерфейсы и классы (например, GetBooksByISBN / Customer / Date / Wh независимо от того, что просто формирует объект GetBooksParameters и вызывает оказание услуг). Комбо было отличным.
BlackjacketMack
1
@IsaacKleinman Я не могу вспомнить, кто из великих написал это (Боб Мартин?), Но это фундаментальный вопрос: хотите ли вы Oven.Bake (пицца) или Pizza.Bake (печь). И ответ «это зависит». Обычно мы хотим, чтобы внешняя служба (или единица работы) управляла одним или несколькими объектами (или пиццей!). Но кто скажет, что эти отдельные объекты не имеют возможности реагировать на тип печи, в которой они выпекаются. Я предпочитаю OrderRepository.Save (order) вместо Order.Save (). Тем не менее, мне нравится Order.Validate (), потому что заказ может знать свою идеальную форму. Контекстный и личный.
БлэкджекМак
2

Вот как я это делал, хотя я внедряю провайдера данных как универсальный интерфейс службы данных, чтобы я мог поменять местами реализации.

Насколько я знаю, контроллер предназначен для того, чтобы получать данные, выполнять какие-либо действия и передавать данные в представление.

Натан Крэддок
источник
Да, я читал об использовании «сервисного интерфейса» для провайдера данных, потому что он помогает с модульным тестированием.
scott.korin