Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active September 14, 2026 12:52
Show Gist options
  • Select an option

  • Save sunmeat/e5f869782be6344202978c8e8b8c3e82 to your computer and use it in GitHub Desktop.

Select an option

Save sunmeat/e5f869782be6344202978c8e8b8c3e82 to your computer and use it in GitHub Desktop.
Практика: 3-рівнева архітектура, рівні BLL та PL

3-рівнева архітектура в ASP.NET Core MVC

Розробка Business Logic Layer та Presentation Layer

Веб-додаток «Кінопошук» / «10 найкращих фільмів»


Передумови

У попередньому практичному завданні було реалізовано Data Access Layer (DAL) для веб-додатку «Кінопошук»:

  • загальний інтерфейс репозиторію IRepository<T> та базова реалізація Repository<T>;
  • спеціалізовані репозиторії за потреби;
  • інтерфейс IUnitOfWork та реалізація UnitOfWork;
  • використання єдиного екземпляра DbContext;
  • реєстрація залежностей у DI-контейнері.

Тепер необхідно завершити побудову трирівневої архітектури, реалізувавши:

  1. Business Logic Layer / Service Layer (BLL) — шар бізнес-логіки.
  2. Presentation Layer (PL) — шар представлення та взаємодії з користувачем.

Логічна структура застосунку:

Presentation Layer
    ↓
Business Logic Layer
    ↓
Data Access Layer
    ↓
Database

Мета практичного завдання

Закріпити принципи багаторівневої архітектури в ASP.NET Core та навчитися:

  • відокремлювати бізнес-логіку від контролерів;
  • працювати з DAL через абстракції;
  • організовувати взаємодію між сервісами та репозиторіями;
  • реалізовувати валідацію та бізнес-правила;
  • використовувати DTO та ViewModel;
  • створювати тонкі контролери;
  • правильно обробляти помилки;
  • організовувати залежності через Dependency Injection;
  • підготувати застосунок до подальшої трансформації в Web API та React-клієнт.

Частина 1. Реалізація Business Logic Layer

1. Аналіз існуючої структури

Проаналізувати поточний проєкт та визначити:

  • які класи належать до 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

2. Створення інтерфейсу сервісу

Створити інтерфейс 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.
  • Бізнес-логіка не повинна бути розміщена безпосередньо в контролері.

3. Реалізація сервісу фільмів

Створити клас 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)
    {
        // пошук фільму, видалення, збереження змін
    }
}

4. Реалізація DTO

Створити 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;
  • складність зміни моделі даних;
  • змішування моделей зберігання та моделей представлення.

5. Реалізація бізнес-правил

У сервісі реалізувати щонайменше такі правила:

  1. Назва фільму не може бути порожньою.
  2. Рік випуску повинен бути коректним.
  3. Рік випуску не може бути більшим за поточний рік.
  4. Не можна створити фільм із дубльованою назвою, якщо така вимога передбачена логікою застосунку.
  5. Неможливо відредагувати або видалити фільм, якого не існує.
  6. Постер повинен мати допустимий формат та розмір.
  7. Опис фільму не повинен перевищувати встановлену довжину.

Бізнес-правила повинні знаходитися в BLL, а не дублюватися в кожному контролері.

Перевірки формату HTTP-запиту та відображення помилок належать до Presentation Layer. Перевірки, пов’язані з правилами предметної області, належать до BLL.


6. Робота із завантаженням постерів

Перенести логіку роботи з файлами із контролера до сервісного шару або окремого сервісу.

Рекомендується створити окрему абстракцію:

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-тип;
  • обмежити максимальний розмір файлу;
  • генерувати унікальне ім’я;
  • коректно обробляти ситуацію, коли постер не завантажено;
  • під час оновлення не видаляти старий постер до моменту успішного збереження нового;
  • під час видалення фільму передбачити видалення пов’язаного файлу.

7. Обробка помилок у BLL

Визначити підхід до обробки помилок.

Можливі варіанти:

  • повернення null або false для передбачуваних ситуацій;
  • використання власних винятків;
  • використання спеціального результату операції;
  • централізована обробка винятків у Presentation Layer.

Рекомендується передбачити окремі типи помилок, наприклад:

public class MovieNotFoundException : Exception
{
    public MovieNotFoundException(int id)
        : base($"Фільм з ідентифікатором {id} не знайдено.")
    {
    }
}

Необхідно пояснити:

  • які помилки є очікуваними;
  • які помилки повинні відображатися користувачу;
  • які помилки не можна показувати у вигляді технічного stack trace;

8. Реєстрація BLL у DI-контейнері

Створити 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.


Частина 2. Реалізація Presentation Layer

9. Рефакторинг контролера

Оновити 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-відповідь;
  • виконувати навігацію та відображення помилок.

10. Реалізація ViewModel для форм

Створити окремі 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-форми без обґрунтування.
  • Для редагування необхідно передбачити ідентифікатор фільму.

11. Реалізація CRUD у Presentation Layer

Реалізувати повний набір дій у MoviesController.

Index

  • отримати список фільмів через IMovieService;
  • передати дані у View;
  • відобразити фільми у вигляді сітки або списку;
  • показати постери, назви, режисерів, жанри та роки випуску.

Details

  • прийняти id;
  • отримати фільм через сервіс;
  • якщо фільм не знайдено, повернути NotFound();
  • відобразити детальну інформацію.

Create GET

  • відобразити форму створення;
  • підготувати необхідні списки або довідники;
  • передати дані у View.

Create POST

  • прийняти ViewModel;
  • перевірити ModelState;
  • передати дані сервісу;
  • обробити результат;
  • перенаправити на список після успішного створення;
  • відобразити помилки у разі невдачі.

Edit GET

  • отримати фільм за id;
  • якщо фільм не знайдено, повернути NotFound();
  • сформувати ViewModel;
  • відобразити форму редагування.

Edit POST

  • прийняти id та ViewModel;
  • перевірити відповідність ідентифікаторів;
  • перевірити ModelState;
  • передати дані сервісу;
  • обробити результат;
  • перенаправити після успішного оновлення.

Delete GET

  • отримати фільм;
  • відобразити сторінку підтвердження;
  • не виконувати видалення під час GET-запиту.

Delete POST

  • прийняти ідентифікатор;
  • викликати сервіс видалення;
  • обробити результат;
  • перенаправити до списку.

12. Валідація на рівні Presentation Layer

Реалізувати клієнтську та серверну валідацію форм.

Необхідно використати:

  • 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;
  • некоректно введені дані не повинні призводити до втрати інших значень форми;
  • після помилки завантаження файлу користувач повинен отримати зрозуміле повідомлення.

13. Відображення повідомлень про результат операції

Реалізувати повідомлення для користувача після виконання операцій:

  • фільм успішно створено;
  • фільм успішно оновлено;
  • фільм успішно видалено;
  • фільм не знайдено;
  • фільм із такою назвою вже існує;
  • файл має недопустимий формат;
  • файл перевищує максимальний розмір.

Для передачі одноразових повідомлень можна використати TempData.

Приклад:

TempData["SuccessMessage"] = "Фільм успішно додано.";

У спільному шаблоні або View реалізувати відображення повідомлень.


14. Авторизація доступу до CRUD

За можливості додати розмежування доступу:

  • перегляд фільмів доступний усім користувачам;
  • створення, редагування та видалення доступні лише авторизованим користувачам або адміністраторам.

Можливий приклад:

[Authorize]
public IActionResult Create()
{
    return View();
}

Для окремих дій можна використати політики або ролі.

Якщо авторизація ще не розглядалася, цей пункт можна виконати як додаткове завдання.


15. Обробка помилок у Presentation Layer

Передбачити коректну реакцію застосунку на типові ситуації:

  • запис не знайдено;
  • користувач передав некоректний id;
  • сервіс повернув помилку бізнес-правила;
  • сталася помилка під час роботи з файловою системою;
  • сталася помилка під час збереження до БД.

Необхідно:

  • не показувати користувачу технічні деталі винятків у production-режимі;
  • використовувати відповідні HTTP-результати (NotFound, BadRequest, RedirectToAction, тощо);
  • за потреби створити сторінку загальної помилки;

Частина 3. Інтеграція всіх шарів

16. Перевірка взаємодії між шарами

Перевірити, що запит проходить через усі рівні за правильною схемою:

HTTP Request
    ↓
Controller
    ↓
IMovieService
    ↓
MovieService
    ↓
IUnitOfWork
    ↓
IRepository<Movie>
    ↓
MovieDbContext
    ↓
SQL Server

Під час повернення відповіді дані повинні проходити у зворотному напрямку.

Необхідно перевірити, що:

  • Presentation Layer не залежить напряму від DAL;
  • BLL не залежить від конкретного контролера;
  • DAL не містить HTML або HTTP-логіки;
  • бізнес-правила не дублюються у View;
  • залежності реєструються через DI;
  • усі CRUD-операції працюють через сервіс.

17. Перевірка життєвого циклу DbContext

Перевірити, що 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 вручну в кожному методі сервісу;
  • які проблеми можуть виникнути при неправильному життєвому циклі залежностей.

Частина 4. Додаткові завдання

18. Пошук, фільтрація та сортування

Додати можливість:

  • пошуку фільмів за назвою;
  • фільтрації за жанром;
  • фільтрації за режисером;
  • сортування за назвою;
  • сортування за роком випуску.

Реалізацію необхідно виконати з дотриманням розподілу відповідальності:

  • параметри пошуку приймає Presentation Layer;
  • правила формування запиту та обробка даних належать BLL;
  • виконання запиту до БД належить DAL.

19. Пагінація

Реалізувати посторінкове відображення списку фільмів.

Передбачити:

  • номер поточної сторінки;
  • розмір сторінки;
  • загальну кількість записів;
  • навігацію між сторінками.

Пояснити, на якому рівні повинна виконуватися пагінація та чому завантаження всіх записів у пам’ять може бути неефективним.


Очікуваний результат

Після виконання практичного завдання студент повинен отримати функціональний веб-додаток «Кінопошук» із трирівневою архітектурою:

Presentation Layer
    ├── Controllers
    ├── Views
    └── ViewModels

Business Logic Layer
    ├── Interfaces
    ├── Services
    └── DTOs

Data Access Layer
    ├── IRepository<T>
    ├── Repository<T>
    ├── IUnitOfWork
    ├── UnitOfWork
    └── DbContext

Застосунок повинен підтримувати:

  • перегляд списку фільмів;
  • перегляд деталей фільму;
  • створення фільму;
  • редагування фільму;
  • видалення фільму;
  • завантаження та збереження постерів;
  • серверну та клієнтську валідацію;
  • обробку помилок;
  • взаємодію між шарами через абстракції;
  • роботу через Dependency Injection.

Вимоги до здачі

Необхідно надати:

  1. Посилання на GitHub-репозиторій із проєктом.
  2. Опис реалізованої архітектури.
  3. Схему взаємодії шарів.
  4. Приклади інтерфейсів та основних класів BLL.
  5. Приклади контролера та ViewModel.
  6. Скріншоти основних сторінок:
    • список фільмів;
    • деталі;
    • створення;
    • редагування;
    • підтвердження видалення;
    • приклади повідомлень про помилки.
  7. Коротке пояснення:
    • чому контролер повинен бути тонким;
    • навіщо потрібні DTO та ViewModel;
    • яку відповідальність має BLL;
    • яку відповідальність має DAL;
    • як забезпечується слабка зв’язаність;
    • як реалізується принцип єдиної відповідальності.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment