Перейти к содержанию

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 строк и в нём появились расчёты, фильтрация, создание сущностей и работа с файлами, это сигнал: часть кода нужно перенести в сервис или инфраструктурный класс.