1. Архитектура приложения¶
Многослойная архитектура нужна, чтобы код не превращался в один большой контроллер. Контроллер принимает HTTP-запрос, сервис выполняет сценарий, репозиторий работает с базой, доменная модель хранит данные и правила.
В учебных проектах часто всё пишут в одном файле: контроллер сразу проверяет данные, создаёт SQL-запрос, сохраняет запись и формирует ответ. На маленьком примере это кажется удобным, но в производственной практике такой подход быстро ломается. Любое изменение начинает задевать сразу всё приложение.
Многослойная архитектура решает эту проблему через разделение ответственности. Каждый слой отвечает за свою часть работы и не пытается выполнять чужие обязанности.
Пример из практики:
- контроллер знает, что пришёл запрос
POST /api/events; - сервис знает, как правильно создать событие активности;
- репозиторий знает, как сохранить событие в базу;
- доменная сущность знает, какие поля есть у события.
Такой подход делает код понятнее для команды: один студент может работать над API, другой над аналитикой, третий над базой данных, и их изменения будут меньше мешать друг другу.
Структура проектов¶
Команды ниже создают не просто несколько папок, а основу многослойной архитектуры. Отдельные проекты позволяют явно контролировать зависимости: например, Domain не сможет случайно обратиться к HTTP-контроллеру или EF Core.
dotnet new sln -n ActivityMonitoring
dotnet new webapi -n ActivityMonitoring.Api
dotnet new classlib -n ActivityMonitoring.Domain
dotnet new classlib -n ActivityMonitoring.Application
dotnet new classlib -n ActivityMonitoring.Infrastructure
dotnet sln add ActivityMonitoring.Api ActivityMonitoring.Domain ActivityMonitoring.Application ActivityMonitoring.Infrastructure
dotnet add ActivityMonitoring.Application reference ActivityMonitoring.Domain
dotnet add ActivityMonitoring.Infrastructure reference ActivityMonitoring.Application ActivityMonitoring.Domain
dotnet add ActivityMonitoring.Api reference ActivityMonitoring.Application ActivityMonitoring.Infrastructure
Domain: сущность события¶
Слой Domain содержит основные понятия предметной области. В нашем случае важнейшее понятие — событие активности. Это не “таблица базы данных ради таблицы”, а запись о действии пользователя в системе.
Событие отвечает на вопросы:
- кто выполнил действие;
- что произошло;
- откуда пришло действие;
- насколько оно важно;
- когда оно произошло.
Именно поэтому в классе есть только смысловые поля события, без атрибутов маршрутизации, SQL-запросов и других технических деталей внешних слоёв.
namespace ActivityMonitoring.Domain.Entities;
public class ActivityEvent
{
public int Id { get; set; }
public int UserId { get; set; }
public AppUser User { get; set; } = null!;
public string Type { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Severity { get; set; } = "Info";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Application: DTO¶
DTO отделяет внешний контракт API от внутренней EF-модели.
Это полезно по двум причинам. Во-первых, клиенту не нужно видеть все внутренние поля сущности. Во-вторых, структура ответа API может отличаться от структуры таблицы. Например, в таблице есть UserId, а в ответ удобно добавить UserName.
Код ниже показывает эту границу: один тип описывает входные данные для создания события, второй — ответ клиенту после чтения из системы.
namespace ActivityMonitoring.Application.Events;
public record CreateActivityEventRequest(
string Type,
string Source,
string Description,
string Severity);
public record ActivityEventDto(
int Id,
int UserId,
string UserName,
string Type,
string Source,
string Description,
string Severity,
DateTime CreatedAt);
Application: интерфейс сервиса¶
Интерфейс описывает, что сервис умеет делать, но не показывает, как именно он это делает. Это удобно для тестирования и замены реализации. Контроллеру не важно, работает сервис через SQLite, PostgreSQL или тестовую коллекцию в памяти.
namespace ActivityMonitoring.Application.Events;
public interface IActivityEventService
{
Task<ActivityEventDto> CreateAsync(CreateActivityEventRequest request, int userId, CancellationToken ct);
Task<IReadOnlyList<ActivityEventDto>> GetAsync(ActivityEventFilter filter, CancellationToken ct);
Task<bool> DeleteAsync(int id, CancellationToken ct);
}
Presentation: контроллер¶
Слой Presentation принимает внешние запросы. В ASP.NET Core этим чаще всего занимаются контроллеры. Контроллер должен быть тонким: он не решает бизнес-задачу сам, а передаёт её в сервис.
В примере контроллер только связывает HTTP-маршруты с методами сервиса и выбирает подходящий HTTP-ответ. Поэтому в нём нет кода сохранения в базу и сложных правил обработки события.
[ApiController]
[Route("api/events")]
public class EventsController : ControllerBase
{
private readonly IActivityEventService _service;
public EventsController(IActivityEventService service)
{
_service = service;
}
[HttpPost]
public async Task<ActionResult<ActivityEventDto>> Create(CreateActivityEventRequest request, CancellationToken ct)
{
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var created = await _service.CreateAsync(request, userId, ct);
return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
}
}
Главное правило¶
Контроллер не должен сам писать SQL, считать аналитику или решать бизнес-правила. Он только принимает запрос, вызывает сервис и возвращает HTTP-ответ.
Если при разработке вы замечаете, что метод контроллера стал длиннее 30-40 строк и в нём появились расчёты, фильтрация, создание сущностей и работа с файлами, это сигнал: часть кода нужно перенести в сервис или инфраструктурный класс.