Есть сайт на ASP.Net Core MVC. У него в шапке всегда есть выдвижной пункт меню со списком жанров, также там есть блок авторизованного пользователя (аватарка, ник, кнопка разлогина).
То есть, модель каждой View должна содержать в себе поле списка жанров и поле класса пользователя. Первое, что пришло мне в голову, это создание базового класса с этими полями, вроде этого:
public class BaseViewModel
{
private ISession session;
private TMDbContext context;
public BaseViewModel(ISession session, TMDbContext context)
{
this.session = session;
this.context = context;
}
public User CurrentUser => session?.GetJson<User>("CurrentUser");
public IQueryable<Genre> Genres => context.Genres;
}
Но при попытке создать первый же производный класс и использовать его в контроллере сталкиваешься с некоторыми проблемами. Если сессию легко получить в каждом контроллере без каких-либо манипуляций из HttpContext.Session, то контекст у тебя в контроллере вообще не фигурирует явно. Все данные у меня идут через реализации интерфейсов (по рекомендации из учебника Фримена), с привязкой в Startup.cs типа
services.AddDbContext<TMDbContext>(option =>
option.UseNpgsql(
Configuration["Data:Movies:ConnectionString"]));
Поэтому выходит, что только ради работы шапки, мне будет необходимо запрашивать в конструкторе контроллера данные, которые будут выделяться из контекста, но нужны лишь для того, чтобы передать их в базовую ViewModel.
Например, запрашивать жанры в UserController, который занимается регистрацией/авторизацией.
В связи со всем вышесказанным у меня возникает ощущение, что я мыслю не в том направлении и есть куда более правильное и элегантное решение.
Как и где делать привязку данных, которые нужны для шаблона, т.е. при отображении любой View?
Класс связки данных с контекстом БД:
public interface IUserRepository
{
IQueryable<User> Users { get; }
IQueryable<UserRate> UserRates { get; }
void SaveUser(User user);
void SaveUserRate(UserRate userRate);
}
public class EFUserRepository : IUserRepository
{
private TMDbContext context;
public EFUserRepository(TMDbContext context)
{
this.context = context;
}
public IQueryable<User> Users => context.Users;
public IQueryable<UserRate> UserRates => context.UserRates;
public void SaveUser(User user)
{
context.Users.Add(user);
context.SaveChanges();
}
public void SaveUserRate(UserRate userRate)
{
context.UserRates.Add(userRate);
context.SaveChanges();
}
}
Вызов этого класса в конструкторе контроллера:
public class UserController : Controller
{
private IUserRepository repository;
public UserController(IUserRepository repository)
{
this.repository = repository; // в Startip.cs привязка через: services.AddTransient<IMovieRepository, EFMovieRepository>();
}
public IActionResult Index()
{
return View();
}
}
На самом деле, нет ничего плохого в том чтобы запросить жанры из вида минуя контроллер. Ведь контроллер - это компонент который обрабатывает пользовательский ввод (в случае веб-приложений - это HTTP-запросы), он не является почтальоном между моделями и видами.
Важное замечание: когда я говорю "запросить" - я имею в виду запросить у репозитория, а не вытащить из базы. В представлении не должно быть ничего кроме логики отображения.
В соответствии с документацией, для этого можно использовать директиву @inject:
@inject IGenresRepository Genres
<ul>
@foreach (var genre in Genres.All) {
<li>@genre.Name</li>
}
</ul>
Основа MVC - это разделение отображения данных и логики получения данных.
Смотрите. У вас есть какой-то контроллер, например, FilmsController, в нём есть Action отображающий страницу детального описания фильма, скажем, Detail.
Правильный порядок действий в этом Action: считать всё из базы данных, наполнить модель данными, отключиться от базы, передать модель во View.
За попытки залезть в базу данных из View или Model нужно расстреливать на кодревью и заставлять тысячу раз писать мелом на доске "Смысл MV*-паттернов в разделении кода бизнес-логики и отображения". Не надо во вью или модели рассчитывать, что у вас есть доступ к базе на "щас ещё чуть-чуть дочитаю", раньше надо было.
Поэтому ваша базовая модель, если вам реально ВЕЗДЕ нужен список жанров должна выглядеть так:
public class BaseViewModel
{
public IReadOnlyCollection<Genre> Genres { get; set; }
}
Всё, наследуйтесь на здоровье.
И разумеется, у вас будет базовый контроллер, от которого вы будете наследовать каждый контроллер, в котором нужны будут жанры и в котором нужно прописать код получения жанров.
По-грамотному, каждый контроллер должен иметь в зависимостях те сервисы и репозитории, которые ему реально понадобятся для работы, а в зависимостях базового контроллера у вас будет ваш сервис жанров.
Но понятное дело, что гораздо проще обойтись вообще без сервисов и репозиториев, создавать в базовом контроллере контекст и пользоваться этим контекстом в каждом контроллере напрямую. Кодогенерация в Asp.Net MVC как раз содержит пример такого CRUD-контроллера.
Update. По поводу замечания Павла Майорова:
Почему контроллер вообще должен знать что на странице есть шапка со списком жанров? Шапка - это ответственность исключительно представления, а не контроллера. Должна быть возможность сменить шапку у сайта не переписывая все контроллеры!
В общем, я считаю что выборка всех данных в контроллере нарушает SRP.
В принципе, можно сделать ещё более правильным образом.
Во-первых, создайте контроллер GenreController, пропишите нужный метод получения жанров и пометьте его атрибутом [ChildActionOnly]
Где-то в общем Layout страницы в div'е с шапкой страницы можете вставить показ списка жанров через @Html.Action(метод, контроллер жанров).
Апостиль в Лос-Анджелесе без лишних нервов и бумажной волокиты
Основные этапы разработки сайта для стоматологической клиники
Продвижение своими сайтами как стратегия роста и независимости