У попередньому практичному завданні було реалізовано Data Access Layer (DAL) для веб-додатку «Кінопошук»:
- загальний інтерфейс репозиторію
IRepository<T>та базова реалізаціяRepository<T>; - спеціалізовані репозиторії за потреби;
- інтерфейс
IUnitOfWorkта реалізаціяUnitOfWork; - використання єдиного екземпляра
DbContext; - реєстрація залежностей у DI-контейнері.
Тепер необхідно завершити побудову трирівневої архітектури, реалізувавши:
- Business Logic Layer / Service Layer (BLL) — шар бізнес-логіки.
- Presentation Layer (PL) — шар представлення та взаємодії з користувачем.
Логічна структура застосунку:
Presentation Layer
↓
Business Logic Layer
↓
Data Access Layer
↓
Database
Закріпити принципи багаторівневої архітектури в ASP.NET Core та навчитися:
- відокремлювати бізнес-логіку від контролерів;
- працювати з DAL через абстракції;
- організовувати взаємодію між сервісами та репозиторіями;
- реалізовувати валідацію та бізнес-правила;
- використовувати DTO та ViewModel;
- створювати тонкі контролери;
- правильно обробляти помилки;
- організовувати залежності через Dependency Injection;
- підготувати застосунок до подальшої трансформації в Web API та React-клієнт.
Проаналізувати поточний проєкт та визначити:
- які класи належать до DAL;
- які класи відповідають за бізнес-логіку;
- які класи належать до Presentation Layer;
- де зараз виконується валідація;
- де реалізовано роботу із завантаженням та видаленням постерів;
- чи містить контролер прямі звернення до
DbContext, репозиторіїв або файлової системи.
Сформувати цільову структуру проєкту.
Орієнтовний варіант:
MovieSearch.sln
│
├── MovieSearch.API/ # Presentation Layer (Web API)
│ ├── Controllers/
│ │ └── MoviesController.cs
│ ├── Program.cs
│ └── MovieSearch.API.csproj
│
├── MovieSearch.BLL/ # Core / Business Logic Layer
│ ├── Interfaces/
│ │ └── IMovieService.cs
│ ├── Services/
│ │ └── MovieService.cs
│ ├── DTOs/
│ │ └── MovieDto.cs
│ └── MovieSearch.BLL.csproj
│
└── MovieSearch.DAL/ # Infrastructure / Data Access Layer
├── Entities/ # Перенесено з Models (Movie, Genre, Director)
│ ├── Movie.cs
│ ├── Genre.cs
│ └── Director.cs
├── Interfaces/
│ ├── IRepository.cs
│ └── IUnitOfWork.cs
├── Repositories/
│ └── Repository.cs
├── UnitOfWork.cs
├── MovieDbContext.cs
└── MovieSearch.DAL.csproj
Створити інтерфейс IMovieService, який описує операції бізнес-рівня для роботи з фільмами.
Інтерфейс повинен містити методи для:
- отримання всіх фільмів;
- отримання фільму за ідентифікатором;
- створення фільму;
- редагування фільму;
- видалення фільму;
- за потреби — пошуку, фільтрації та сортування.
Орієнтовний приклад:
public interface IMovieService
{
Task<IEnumerable<MovieDto>> GetAllAsync();
Task<MovieDto?> GetByIdAsync(int id);
Task<int> CreateAsync(MovieCreateDto dto);
Task<bool> UpdateAsync(int id, MovieUpdateDto dto);
Task<bool> DeleteAsync(int id);
}Інтерфейс необхідно адаптувати до фактичної структури сутностей та DTO у власному проєкті.
- Сервіс не повинен залежати від
Controller,View,HttpContextабоModelState. - Сервіс повинен працювати через
IUnitOfWorkабо іншу абстракцію DAL. - Бізнес-логіка не повинна бути розміщена безпосередньо в контролері.
Створити клас MovieService, який реалізує IMovieService.
Сервіс повинен:
- отримувати залежність
IUnitOfWorkчерез конструктор; - виконувати операції з фільмами через репозиторії;
- викликати
SaveChangesAsync()у відповідних операціях; - перетворювати сутності на DTO або інші об’єкти, які використовує Presentation Layer;
- містити бізнес-правила застосунку.
Орієнтовна структура:
public class MovieService : IMovieService
{
private readonly IUnitOfWork _unitOfWork;
public MovieService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<MovieDto>> GetAllAsync()
{
// отримання фільмів через DAL
// перетворення сутностей у DTO
}
public async Task<MovieDto?> GetByIdAsync(int id)
{
// отримання фільму за ідентифікатором
}
public async Task<int> CreateAsync(MovieCreateDto dto)
{
// перевірка бізнес-правил
// створення сутності
// збереження змін
}
public async Task<bool> UpdateAsync(int id, MovieUpdateDto dto)
{
// пошук фільму, перевірка бізнес-правил, оновлення, збереження змін
}
public async Task<bool> DeleteAsync(int id)
{
// пошук фільму, видалення, збереження змін
}
}Створити DTO для передачі даних між шарами.
Рекомендується реалізувати щонайменше такі класи:
MovieDto;MovieCreateDto;MovieUpdateDto.
Приклад:
public class MovieDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Director { get; set; } = string.Empty;
public string Genre { get; set; } = string.Empty;
public int ReleaseYear { get; set; }
public string? PosterPath { get; set; }
public string? Description { get; set; }
}Для створення та редагування можна використовувати окремі DTO, оскільки набір доступних полів може відрізнятися.
Пояснити, чому передавання сутності Movie безпосередньо між усіма шарами може бути небажаним. Розглянути такі проблеми:
- надмірна залежність шарів від структури БД;
- можливість випадкового редагування службових полів;
- ризик overposting;
- складність зміни моделі даних;
- змішування моделей зберігання та моделей представлення.
У сервісі реалізувати щонайменше такі правила:
- Назва фільму не може бути порожньою.
- Рік випуску повинен бути коректним.
- Рік випуску не може бути більшим за поточний рік.
- Не можна створити фільм із дубльованою назвою, якщо така вимога передбачена логікою застосунку.
- Неможливо відредагувати або видалити фільм, якого не існує.
- Постер повинен мати допустимий формат та розмір.
- Опис фільму не повинен перевищувати встановлену довжину.
Бізнес-правила повинні знаходитися в BLL, а не дублюватися в кожному контролері.
Перевірки формату HTTP-запиту та відображення помилок належать до Presentation Layer. Перевірки, пов’язані з правилами предметної області, належать до BLL.
Перенести логіку роботи з файлами із контролера до сервісного шару або окремого сервісу.
Рекомендується створити окрему абстракцію:
public interface IFileService
{
Task<string> SaveAsync(IFormFile file);
Task DeleteAsync(string? filePath);
}Можлива реалізація:
public class FileService : IFileService
{
private readonly IWebHostEnvironment _environment;
public FileService(IWebHostEnvironment environment)
{
_environment = environment;
}
public async Task<string> SaveAsync(IFormFile file)
{
// перевірка розширення та розміру
// створення унікального імені
// збереження файлу
// повернення відносного шляху
}
public Task DeleteAsync(string? filePath)
{
// видалення файлу за потреби
}
}- не використовувати оригінальне ім’я файлу як єдине ім’я для збереження;
- перевіряти розширення файлу;
- перевіряти MIME-тип;
- обмежити максимальний розмір файлу;
- генерувати унікальне ім’я;
- коректно обробляти ситуацію, коли постер не завантажено;
- під час оновлення не видаляти старий постер до моменту успішного збереження нового;
- під час видалення фільму передбачити видалення пов’язаного файлу.
Визначити підхід до обробки помилок.
Можливі варіанти:
- повернення
nullабоfalseдля передбачуваних ситуацій; - використання власних винятків;
- використання спеціального результату операції;
- централізована обробка винятків у Presentation Layer.
Рекомендується передбачити окремі типи помилок, наприклад:
public class MovieNotFoundException : Exception
{
public MovieNotFoundException(int id)
: base($"Фільм з ідентифікатором {id} не знайдено.")
{
}
}Необхідно пояснити:
- які помилки є очікуваними;
- які помилки повинні відображатися користувачу;
- які помилки не можна показувати у вигляді технічного stack trace;
Створити extension-метод для реєстрації сервісів.
Наприклад:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBusinessLogic(
this IServiceCollection services)
{
services.AddScoped<IMovieService, MovieService>();
services.AddScoped<IFileService, FileService>();
return services;
}
}Підключити реєстрацію в Program.cs:
builder.Services.AddBusinessLogic();Перевірити, що всі залежності коректно створюються контейнером DI.
Оновити MoviesController таким чином, щоб він працював через IMovieService.
Контролер не повинен:
- напряму звертатися до
DbContext; - виконувати SQL-запити;
- працювати з репозиторіями безпосередньо;
- містити складну бізнес-логіку;
- самостійно реалізовувати збереження файлів;
- містити дубльовані бізнес-перевірки.
Орієнтовна структура:
public class MoviesController : Controller
{
private readonly IMovieService _movieService;
public MoviesController(IMovieService movieService)
{
_movieService = movieService;
}
public async Task<IActionResult> Index()
{
var movies = await _movieService.GetAllAsync();
return View(movies);
}
public async Task<IActionResult> Details(int id)
{
var movie = await _movieService.GetByIdAsync(id);
if (movie is null)
{
return NotFound();
}
return View(movie);
}
}Контролер повинен виконувати переважно такі завдання:
- приймати HTTP-запит;
- отримувати параметри;
- передавати дані сервісу;
- перевіряти результат операції;
- повертати View або HTTP-відповідь;
- виконувати навігацію та відображення помилок.
Створити окремі ViewModel для сторінок створення та редагування.
Рекомендується реалізувати:
MovieCreateViewModel;MovieEditViewModel;MovieDetailsViewModel.
Приклад:
public class MovieCreateViewModel
{
[Required(ErrorMessage = "Вкажіть назву фільму.")]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Вкажіть режисера.")]
public string Director { get; set; } = string.Empty;
[Required(ErrorMessage = "Вкажіть жанр.")]
public string Genre { get; set; } = string.Empty;
[Range(1888, 2100)]
public int ReleaseYear { get; set; }
public IFormFile? Poster { get; set; }
[StringLength(2000)]
public string? Description { get; set; }
}- ViewModel повинна містити лише дані, необхідні для конкретної форми.
- Не слід передавати
IFormFileу сутність бази даних. - Не слід використовувати Entity Framework-сутність як модель HTML-форми без обґрунтування.
- Для редагування необхідно передбачити ідентифікатор фільму.
Реалізувати повний набір дій у MoviesController.
- отримати список фільмів через
IMovieService; - передати дані у View;
- відобразити фільми у вигляді сітки або списку;
- показати постери, назви, режисерів, жанри та роки випуску.
- прийняти
id; - отримати фільм через сервіс;
- якщо фільм не знайдено, повернути
NotFound(); - відобразити детальну інформацію.
- відобразити форму створення;
- підготувати необхідні списки або довідники;
- передати дані у View.
- прийняти ViewModel;
- перевірити
ModelState; - передати дані сервісу;
- обробити результат;
- перенаправити на список після успішного створення;
- відобразити помилки у разі невдачі.
- отримати фільм за
id; - якщо фільм не знайдено, повернути
NotFound(); - сформувати ViewModel;
- відобразити форму редагування.
- прийняти
idта ViewModel; - перевірити відповідність ідентифікаторів;
- перевірити
ModelState; - передати дані сервісу;
- обробити результат;
- перенаправити після успішного оновлення.
- отримати фільм;
- відобразити сторінку підтвердження;
- не виконувати видалення під час GET-запиту.
- прийняти ідентифікатор;
- викликати сервіс видалення;
- обробити результат;
- перенаправити до списку.
Реалізувати клієнтську та серверну валідацію форм.
Необхідно використати:
- Data Annotations;
Validation Message Tag Helper;Validation Summary Tag Helper;- перевірку
ModelState.IsValid; - клієнтські скрипти unobtrusive validation.
Приклад у View:
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="Title" class="form-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>Підключити необхідні скрипти внизу сторінки:
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}- помилки повинні бути зрозумілими користувачу;
- повідомлення повинні відображатися біля відповідних полів;
- загальні помилки повинні відображатися у validation summary;
- некоректно введені дані не повинні призводити до втрати інших значень форми;
- після помилки завантаження файлу користувач повинен отримати зрозуміле повідомлення.
Реалізувати повідомлення для користувача після виконання операцій:
- фільм успішно створено;
- фільм успішно оновлено;
- фільм успішно видалено;
- фільм не знайдено;
- фільм із такою назвою вже існує;
- файл має недопустимий формат;
- файл перевищує максимальний розмір.
Для передачі одноразових повідомлень можна використати TempData.
Приклад:
TempData["SuccessMessage"] = "Фільм успішно додано.";У спільному шаблоні або View реалізувати відображення повідомлень.
За можливості додати розмежування доступу:
- перегляд фільмів доступний усім користувачам;
- створення, редагування та видалення доступні лише авторизованим користувачам або адміністраторам.
Можливий приклад:
[Authorize]
public IActionResult Create()
{
return View();
}Для окремих дій можна використати політики або ролі.
Якщо авторизація ще не розглядалася, цей пункт можна виконати як додаткове завдання.
Передбачити коректну реакцію застосунку на типові ситуації:
- запис не знайдено;
- користувач передав некоректний
id; - сервіс повернув помилку бізнес-правила;
- сталася помилка під час роботи з файловою системою;
- сталася помилка під час збереження до БД.
Необхідно:
- не показувати користувачу технічні деталі винятків у production-режимі;
- використовувати відповідні HTTP-результати (
NotFound,BadRequest,RedirectToAction, тощо); - за потреби створити сторінку загальної помилки;
Перевірити, що запит проходить через усі рівні за правильною схемою:
HTTP Request
↓
Controller
↓
IMovieService
↓
MovieService
↓
IUnitOfWork
↓
IRepository<Movie>
↓
MovieDbContext
↓
SQL Server
Під час повернення відповіді дані повинні проходити у зворотному напрямку.
Необхідно перевірити, що:
- Presentation Layer не залежить напряму від DAL;
- BLL не залежить від конкретного контролера;
- DAL не містить HTML або HTTP-логіки;
- бізнес-правила не дублюються у View;
- залежності реєструються через DI;
- усі CRUD-операції працюють через сервіс.
Перевірити, що DbContext та UnitOfWork зареєстровані з відповідним життєвим циклом.
Рекомендований варіант:
builder.Services.AddDbContext<MovieDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IMovieService, MovieService>();Пояснити:
- чому
DbContextзазвичай реєструється якScoped; - чому
UnitOfWorkповинен використовувати той самий контекст; - чому не слід створювати новий
DbContextвручну в кожному методі сервісу; - які проблеми можуть виникнути при неправильному життєвому циклі залежностей.
Додати можливість:
- пошуку фільмів за назвою;
- фільтрації за жанром;
- фільтрації за режисером;
- сортування за назвою;
- сортування за роком випуску.
Реалізацію необхідно виконати з дотриманням розподілу відповідальності:
- параметри пошуку приймає Presentation Layer;
- правила формування запиту та обробка даних належать BLL;
- виконання запиту до БД належить DAL.
Реалізувати посторінкове відображення списку фільмів.
Передбачити:
- номер поточної сторінки;
- розмір сторінки;
- загальну кількість записів;
- навігацію між сторінками.
Пояснити, на якому рівні повинна виконуватися пагінація та чому завантаження всіх записів у пам’ять може бути неефективним.
Після виконання практичного завдання студент повинен отримати функціональний веб-додаток «Кінопошук» із трирівневою архітектурою:
Presentation Layer
├── Controllers
├── Views
└── ViewModels
Business Logic Layer
├── Interfaces
├── Services
└── DTOs
Data Access Layer
├── IRepository<T>
├── Repository<T>
├── IUnitOfWork
├── UnitOfWork
└── DbContext
Застосунок повинен підтримувати:
- перегляд списку фільмів;
- перегляд деталей фільму;
- створення фільму;
- редагування фільму;
- видалення фільму;
- завантаження та збереження постерів;
- серверну та клієнтську валідацію;
- обробку помилок;
- взаємодію між шарами через абстракції;
- роботу через Dependency Injection.
Необхідно надати:
- Посилання на GitHub-репозиторій із проєктом.
- Опис реалізованої архітектури.
- Схему взаємодії шарів.
- Приклади інтерфейсів та основних класів BLL.
- Приклади контролера та ViewModel.
- Скріншоти основних сторінок:
- список фільмів;
- деталі;
- створення;
- редагування;
- підтвердження видалення;
- приклади повідомлень про помилки.
- Коротке пояснення:
- чому контролер повинен бути тонким;
- навіщо потрібні DTO та ViewModel;
- яку відповідальність має BLL;
- яку відповідальність має DAL;
- як забезпечується слабка зв’язаність;
- як реалізується принцип єдиної відповідальності.