Розділення доступу до даних у ASP.NET MVC


35

Я хочу переконатися, що я дотримуюсь галузевих стандартів та найкращих практик з моїм першим реальним тріском на MVC. У цьому випадку це ASP.NET MVC, використовуючи C #.

Я буду використовувати Entity Framework 4.1 для своєї моделі з кодовими першими об'єктами (база даних вже існує), тому буде об’єкт DBContext для отримання даних з бази даних.

У демонстраціях, які я пройшов на веб-сайті asp.net, контролери мають в них код доступу до даних. Мені це не здається правильним, особливо, якщо дотримуватися практики DRY (не повторюй себе).

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

Деякі дії можуть мати номер 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);
    }
}

Натомість, чи повинен я насправді в своєму контексті db мати метод повернення однієї книги? Здається, це для мене краще розділення, і це сприяє просуванню 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));
    }
}

Це дійсний набір правил, який слід дотримуватися при кодуванні моєї заявки?

Або, мабуть, більш суб’єктивним було б питання: "це правильний спосіб зробити це?"

Відповіді:


55

Як правило, ви хочете, щоб ваші контролери виконували лише кілька дій:

  1. Обробіть вхідний запит
  2. Делегуйте обробку на якийсь бізнес-об’єкт
  3. Передайте результат бізнес-обробки у відповідний вигляд для візуалізації

Там не повинно бути будь - яких доступ до даних або складної бізнес - логіки в контролері.

[У найпростіших програмах ви, ймовірно, можете відійти від базових дій CRUD даних у своєму контролері, але, як тільки ви почнете додавати більше, ніж прості дзвінки «Отримати та оновити», ви захочете розбити обробку в окремий клас. ]

Ваші контролери зазвичай залежать від "Сервісу", щоб виконати фактичну роботу з обробки. У вашому класі обслуговування ви можете працювати безпосередньо зі своїм джерелом даних (у вашому випадку 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 / Що б тільки не формував об’єкт GetBooksParameters і викликав сервіс). Комбо було чудовим.
BlackjacketMack

1
@IsaacKleinman Я не можу пригадати, хто з великих писав це (Боб Мартін?), Але це основне питання: чи хочете Ви піч Bake (піца) або Pizza.Bake (духовка). А відповідь - це - залежить. Зазвичай ми хочемо, щоб зовнішня служба (або одиниця роботи) маніпулювала одним або кількома об'єктами (або піцами!). Але хто скаже, що ці окремі об'єкти не мають можливості реагувати на тип духовки, в якій випікають. Я віддаю перевагу OrderRepository.Save (замовлення) перед Order.Save (). Однак мені подобається Order.Validate (), оскільки замовлення може знати, що це власна ідеальна форма. Контекстне та особисте.
BlackjacketMack

2

Я це робив, хоча я постачаю дані постачальнику даних як загальний інтерфейс обслуговування даних, щоб я міг поміняти місцями реалізації.

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


Так, я читав про використання "сервісного інтерфейсу" для постачальника даних, оскільки це допомагає при тестуванні одиниць.
scott.korin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.