Backend-разработка на ASP.NET Core: Современный стек

Комплексный курс по созданию производительных веб-сервисов, охватывающий работу с базами данных, кэширование через Redis, безопасность с JWT и контейнеризацию.

1. Архитектура приложения: Dependency Injection, Middleware и настройка окружения

Архитектура приложения: Dependency Injection, Middleware и настройка окружения

Добро пожаловать в курс Backend-разработка на ASP.NET Core: Современный стек. Мы начинаем погружение в одну из самых производительных и гибких платформ для создания веб-приложений. Современный ASP.NET Core — это не просто обновление старых технологий, это полностью переписанный, кроссплатформенный и модульный фреймворк.

В этой первой статье мы разберем фундамент, на котором строится любое приложение: внедрение зависимостей (Dependency Injection), конвейер обработки запросов (Middleware) и конфигурацию окружения. Понимание этих концепций критически важно, так как они определяют, как ваше приложение масштабируется, тестируется и обрабатывает данные.

Точка входа: Program.cs

В современных версиях .NET (начиная с .NET 6 и выше) шаблон приложения был значительно упрощен. Файлы Startup.cs и Program.cs были объединены в один файл Program.cs, использующий стиль top-level statements (инструкции верхнего уровня). Это делает код лаконичным и убирает лишний «шум».

Глобально файл Program.cs делится на две фазы:

  • Фаза регистрации сервисов (DI Container): Здесь мы настраиваем всё, что наше приложение будет использовать (базы данных, кэширование, внешние API).
  • Фаза настройки конвейера (Middleware Pipeline): Здесь мы определяем, как приложение будет обрабатывать входящие HTTP-запросы.
  • Dependency Injection (DI): Сердце приложения

    ASP.NET Core был спроектирован с учетом Dependency Injection с самого начала. Это не надстройка, а базовая часть архитектуры. DI позволяет нам реализовывать принцип Inversion of Control (IoC), делая классы слабо связанными.

    Вместо того чтобы создавать экземпляры зависимостей внутри класса (например, new DatabaseContext()), мы запрашиваем их через конструктор. Фреймворк сам позаботится о создании и уничтожении этих объектов.

    Жизненный цикл сервисов (Service Lifetimes)

    При регистрации сервиса в контейнере (builder.Services) мы обязаны указать его жизненный цикл. Это, пожалуй, самый важный аспект DI, ошибка в котором может привести к утечкам памяти или некорректной работе с данными.

    Существует три основных времени жизни:

  • Transient (Временный)
  • * Создается каждый раз, когда запрашивается. Использование:* Легковесные сервисы без состояния (stateless). Например, сервис для отправки email или конвертации валют. Метод:* AddTransient<IMyService, MyService>()

  • Scoped (В рамках запроса)
  • * Создается один раз на каждый HTTP-запрос. * Все компоненты, обрабатывающие один и тот же запрос, получат один и тот же экземпляр сервиса. Использование:* Контексты баз данных (Entity Framework Core DbContext), сервисы, работающие с данными текущего пользователя. Метод:* AddScoped<IMyService, MyService>()

  • Singleton (Одиночка)
  • * Создается один раз при первом запросе (или при запуске приложения) и живет до завершения работы приложения. * Все запросы используют один и тот же экземпляр. Использование:* Кэширование в памяти, конфигурационные сервисы, синглтоны по паттерну проектирования. Метод:* AddSingleton<IMyService, MyService>()

    !Визуализация различий между Transient, Scoped и Singleton жизненными циклами.

    Опасности захвата зависимостей (Captive Dependencies)

    Важно помнить правило: Сервис с более долгим временем жизни не должен зависеть от сервиса с более коротким временем жизни.

    Если вы внедрите Scoped сервис внутрь Singleton сервиса, то Scoped сервис станет де-факто синглтоном (так как синглтон создается один раз и держит ссылку на зависимость). Это частая ошибка, приводящая к тому, что DbContext не закрывается и накапливает данные из разных запросов.

    Middleware: Конвейер обработки запросов

    Когда HTTP-запрос приходит на сервер, он проходит через цепочку компонентов, называемых Middleware (промежуточное ПО). Каждый компонент может:

  • Обработать запрос и вернуть ответ (прервав цепочку).
  • Выполнить логику и передать запрос следующему компоненту.
  • Выполнить логику после того, как следующий компонент завершил работу (на обратном пути).
  • !Схема прохождения HTTP-запроса через конвейер Middleware.

    Порядок имеет значение

    В Program.cs порядок вызова методов app.Use... критически важен. Middleware выполняются ровно в том порядке, в котором они добавлены.

    Пример классического порядка:

  • UseExceptionHandler (Ловит ошибки от всего, что ниже).
  • UseHsts (Безопасность).
  • UseHttpsRedirection (Перенаправление на HTTPS).
  • UseStaticFiles (Отдача картинок, CSS, JS. Если файл найден, цепочка прерывается здесь).
  • UseRouting (Определение, какой контроллер нужен).
  • UseCors (Настройка кросс-доменных запросов).
  • UseAuthentication (Кто это?).
  • UseAuthorization (Можно ли ему это?).
  • MapControllers (Финальная точка — выполнение бизнес-логики).
  • Если вы поставите UseAuthorization перед UseAuthentication, приложение попытается проверить права доступа у пользователя, которого еще не идентифицировали. Это приведет к ошибке логики.

    Написание собственного Middleware

    Создание своего middleware — отличный способ реализовать сквозную функциональность, например, логирование всех запросов или глобальную обработку исключений.

    Настройка окружения (Configuration)

    Современное приложение не должно хранить настройки (строки подключения, API ключи) в коде. ASP.NET Core предоставляет мощную систему конфигурации, основанную на парах «ключ-значение».

    Источники конфигурации считываются в определенном порядке (последний перезаписывает предыдущие):

  • Файл appsettings.json.
  • Файл appsettings.{Environment}.json (например, appsettings.Development.json).
  • Переменные окружения (Environment Variables).
  • Аргументы командной строки.
  • IConfiguration

    Доступ к настройкам осуществляется через интерфейс IConfiguration. Он регистрируется в DI контейнере автоматически.

    Пример appsettings.json:

    Чтение настроек:

    Паттерн Options

    Для более чистого кода рекомендуется использовать Options Pattern. Вместо того чтобы внедрять IConfiguration в каждый сервис, вы создаете класс, описывающий настройки, и мапите его на секцию конфигурации.

    Это обеспечивает строгую типизацию настроек и защищает от опечаток в строковых ключах.

    Заключение

    Мы рассмотрели три кита архитектуры ASP.NET Core:

    * Dependency Injection позволяет строить модульные и тестируемые приложения, управляя жизненным циклом объектов. * Middleware дает полный контроль над потоком HTTP-запроса, позволяя гибко настраивать безопасность, логирование и маршрутизацию. * Configuration обеспечивает разделение кода и настроек, позволяя безопасно деплоить приложение в разные среды (Dev, Test, Prod).

    В следующей статье мы перейдем к работе с данными и подключим Entity Framework Core для взаимодействия с базой данных.

    2. Работа с данными: Entity Framework Core, PostgreSQL и управление миграциями

    Работа с данными: Entity Framework Core, PostgreSQL и управление миграциями

    В предыдущей статье мы заложили фундамент нашего приложения: настроили внедрение зависимостей (Dependency Injection), разобрались с Middleware и научились читать конфигурацию. Теперь пришло время наполнить наше приложение смыслом — данными.

    Любое серьезное backend-приложение неизбежно работает с базой данных. В мире .NET стандартом де-факто для работы с данными является Entity Framework Core (EF Core), а в качестве системы управления базами данных (СУБД) современные разработчики все чаще выбирают PostgreSQL за его надежность, производительность и открытый исходный код.

    В этой статье мы разберем подход Code-First, научимся проектировать схему БД с помощью классов C# и управлять изменениями через миграции.

    Что такое ORM и EF Core?

    Работа с базой данных напрямую через SQL-запросы (например, используя ADO.NET) дает максимальный контроль, но требует написания огромного количества шаблонного кода. Вам нужно открыть соединение, создать команду, выполнить запрос, прочитать результат и вручную переложить данные из строк таблицы в объекты C#.

    ORM (Object-Relational Mapping) — это технология, которая автоматизирует этот процесс. Она связывает (мапит) объекты в коде с таблицами в реляционной базе данных.

    Entity Framework Core — это современная, кроссплатформенная ORM от Microsoft. Она позволяет работать с данными, используя LINQ (Language Integrated Query), что делает код читаемым и строго типизированным.

    !Визуализация того, как EF Core транслирует операции над объектами C# в SQL-запросы к базе данных.

    Подготовка окружения

    Для начала работы нам понадобятся соответствующие NuGet-пакеты. Поскольку мы используем PostgreSQL, нам нужен провайдер, который "научит" EF Core общаться с этой СУБД.

    Выполните следующие команды в терминале вашего проекта:

    Пакет Microsoft.EntityFrameworkCore.Design необходим для работы инструментов командной строки (создание миграций).

    Моделирование данных (Code-First)

    Существует два подхода к разработке:

  • Database-First: Сначала создается база данных, а потом генерируется код.
  • Code-First: Сначала пишутся классы (сущности), а база данных создается на их основе.
  • В современной разработке чаще используется Code-First, так как это позволяет хранить схему БД в системе контроля версий (Git) вместе с остальным кодом.

    Создадим простую сущность Product:

    EF Core автоматически распознает свойство с именем Id как первичный ключ (Primary Key).

    Контекст базы данных (DbContext)

    Центральным элементом EF Core является класс, наследуемый от DbContext. Это "мост" между вашим кодом и базой данных. Он отвечает за установку соединения, отслеживание изменений в объектах и выполнение запросов.

    Создадим класс ApplicationDbContext:

    Регистрация в DI контейнере

    Теперь нужно зарегистрировать наш контекст в Program.cs, используя знания из предыдущей статьи. Нам также понадобится строка подключения из appsettings.json.

    Добавьте строку подключения в appsettings.json:

    И зарегистрируйте сервис в Program.cs:

    > Важно помнить: AddDbContext регистрирует сервис с жизненным циклом Scoped. Это означает, что новый экземпляр контекста создается для каждого HTTP-запроса и уничтожается в конце запроса. Никогда не пытайтесь внедрить DbContext в Singleton сервис — это приведет к критическим ошибкам параллельного доступа.

    Управление миграциями

    Как только модель данных меняется (например, мы добавили поле Description в класс Product), структура базы данных должна быть обновлена. Миграции позволяют версионировать схему БД.

    Создание первой миграции

    Чтобы создать начальную схему базы данных на основе наших классов, выполните команду:

    Эта команда создаст папку Migrations в проекте с файлами C#, описывающими, какие таблицы и колонки нужно создать.

    Применение миграций

    Чтобы физически создать базу данных и таблицы (или обновить существующие), выполните:

    EF Core преобразует код миграции в SQL-команды (CREATE TABLE...) и выполнит их в PostgreSQL.

    !Пошаговый процесс переноса изменений из кода C# в реальную структуру базы данных.

    Fluent API и настройка модели

    Иногда стандартных соглашений (конвенций) EF Core недостаточно. Например, вы хотите ограничить длину строки или сделать поле обязательным. Это можно сделать двумя способами:

  • Data Annotations: Атрибуты прямо над свойствами класса (например, [Required], [MaxLength(100)]).
  • Fluent API: Настройка в методе OnModelCreating внутри DbContext. Этот способ предпочтительнее, так как он оставляет классы сущностей "чистыми".
  • Пример использования Fluent API в ApplicationDbContext:

    Базовые операции (CRUD)

    Теперь, когда база настроена, давайте посмотрим, как выполнять операции Create, Read, Update, Delete. Обычно этот код находится в сервисах или контроллерах.

    Предположим, мы внедрили ApplicationDbContext _context через конструктор.

    Создание (Create)

    Чтение (Read)

    EF Core использует LINQ для построения запросов. Запрос не выполняется, пока вы не вызовете метод материализации (например, ToListAsync, FirstOrDefaultAsync).

    Обновление (Update)

    Для обновления нужно сначала получить объект, изменить его свойства и сохранить.

    Удаление (Delete)

    Производительность: AsNoTracking

    По умолчанию EF Core отслеживает все объекты, которые вы загружаете из БД, чтобы потом можно было сохранить их изменения. Это требует ресурсов.

    Если вы загружаете данные только для чтения (например, чтобы отдать их клиенту по API) и не собираетесь их менять в этом же запросе, используйте AsNoTracking().

    Это значительно ускоряет выполнение запросов и потребляет меньше памяти.

    Заключение

    Мы подключили PostgreSQL к нашему ASP.NET Core приложению, создали схему базы данных с помощью Code-First подхода и научились выполнять базовые операции с данными.

    Использование EF Core позволяет разработчику сосредоточиться на бизнес-логике, а не на написании SQL-запросов. Однако важно помнить о жизненном цикле контекста и использовать AsNoTracking для оптимизации чтения.

    В следующей статье мы разберем, как правильно проектировать REST API, используя контроллеры, DTO (Data Transfer Objects) и маппинг данных.

    3. Построение REST API: Валидация моделей, Minimal API и обработка ошибок

    Построение REST API: Валидация моделей, Minimal API и обработка ошибок

    В предыдущих статьях мы заложили надежный фундамент: настроили архитектуру приложения, внедрили зависимости через DI и подключили базу данных PostgreSQL с помощью Entity Framework Core. Теперь у нас есть данные, но они «заперты» внутри серверного приложения. Чтобы клиентская часть (веб-сайт, мобильное приложение или другой сервис) могла взаимодействовать с этими данными, нам необходимо построить API (Application Programming Interface).

    В этой статье мы разберем современные подходы к созданию REST API в ASP.NET Core. Мы откажемся от устаревших практик, научимся защищать наши данные с помощью DTO, валидировать входящие запросы и элегантно обрабатывать ошибки.

    Проблема прямых связей: Зачем нужны DTO?

    Одна из самых распространенных ошибок новичков — возвращать сущности базы данных (Entity) напрямую из контроллера или API-эндпоинта.

    Представьте, что у нас есть класс User:

    Если мы вернем этот объект клиенту, мы раскроем PasswordHash и IsAdmin. Это дыра в безопасности. Кроме того, если в будущем мы изменим структуру базы данных, это сломает клиентское приложение.

    Решение — использовать DTO (Data Transfer Objects). Это простые классы, которые содержат только те данные, которые нужны клиенту.

    !Визуализация паттерна DTO: разделение внутренней модели данных и внешнего контракта API.

    Пример DTO для ответа:

    Теперь мы сами контролируем, что видит клиент. Использование record (записей) в C# идеально подходит для DTO, так как они неизменяемы и лаконичны.

    Эволюция API: От контроллеров к Minimal API

    Традиционно в ASP.NET Core использовались Controllers — классы, наследуемые от ControllerBase, помеченные атрибутами [ApiController], [Route] и т.д. Это мощный инструмент, но для простых микросервисов он может быть избыточным («много церемоний»).

    Начиная с .NET 6, Microsoft представила Minimal API. Это подход, позволяющий описывать эндпоинты прямо в Program.cs (или в методах расширения), используя минимум кода. Это не замена контроллерам, а альтернатива, которая работает быстрее и требует меньше кода.

    Сравнение подходов

    Классический контроллер:

    Minimal API:

    Обратите внимание: в Minimal API внедрение зависимостей (DI) работает через параметры лямбда-выражения. Нам не нужно создавать конструктор и поле класса.

    Группировка эндпоинтов

    Чтобы Program.cs не превратился в «простыню» кода, в Minimal API используют метод MapGroup. Это позволяет выносить логику в отдельные модули.

    Валидация данных: FluentValidation

    Никогда не доверяйте данным, пришедшим от клиента. Если вы ожидаете email, проверьте, что это email. Если цену — что она больше нуля.

    Встроенные атрибуты валидации (Data Annotations), такие как [Required], смешивают логику валидации с определением класса. В современном стеке стандартом является библиотека FluentValidation.

    Она позволяет вынести правила проверки в отдельные классы.

    Подключение

    Создание валидатора

    Допустим, у нас есть DTO для создания продукта:

    Создадим для него валидатор:

    Интеграция с Minimal API

    В Minimal API валидация не срабатывает автоматически (в отличие от контроллеров). Нам нужно вызвать валидатор вручную или использовать библиотеку-фильтр. Рассмотрим ручной вариант для понимания процесса:

    Этот код возвращает статус 400 Bad Request со списком ошибок, если данные некорректны.

    Глобальная обработка ошибок

    Что произойдет, если база данных упадет или в коде возникнет NullReferenceException? Клиент увидит страшный HTML с ошибкой 500 или, что хуже, стек-трейс (информацию о внутренностях системы).

    Оборачивать каждый метод в try-catch — плохая практика. Это дублирование кода. Вместо этого мы используем глобальный обработчик.

    !Принцип работы глобального перехвата исключений в конвейере ASP.NET Core.

    IExceptionHandler (Новинка .NET 8)

    В последних версиях .NET появился удобный интерфейс IExceptionHandler. Реализуем его:

    Теперь зарегистрируем его в Program.cs:

    Теперь, где бы ни возникло исключение, клиент получит стандартизированный JSON-ответ (формат ProblemDetails, описанный в RFC 7807).

    Итоговая структура эндпоинта

    Соберем все знания вместе. Хороший эндпоинт в современном ASP.NET Core выглядит так:

  • Принимает DTO.
  • Использует Minimal API.
  • Валидирует данные через FluentValidation.
  • Мапит DTO в Entity (вручную или через AutoMapper).
  • Сохраняет через EF Core.
  • В случае ошибки полагается на Global Exception Handler.
  • Заключение

    Мы научились строить чистый и безопасный API. Использование DTO скрывает структуру БД, FluentValidation выносит правила проверки из бизнес-логики, а Minimal API делает код лаконичным. Глобальная обработка ошибок гарантирует, что клиент всегда получит внятный ответ, даже если сервер «упал».

    В следующей статье мы углубимся в тему производительности и рассмотрим асинхронное программирование: как async/await на самом деле работает под капотом и почему это критично для высоконагруженных систем.

    4. Безопасность системы: Аутентификация через JWT (JSON Web Tokens) и авторизация

    Безопасность системы: Аутентификация через JWT (JSON Web Tokens) и авторизация

    В предыдущих статьях мы прошли путь от настройки архитектуры и базы данных до создания полноценного REST API. Однако сейчас наш API похож на дом с открытыми дверями: любой желающий может зайти, прочитать данные, изменить их или даже удалить. В реальном мире это недопустимо.

    Сегодня мы займемся безопасностью. Мы внедрим механизм, который позволит системе понимать, кто к ней обращается (аутентификация), и решать, что этому пользователю разрешено делать (авторизация). Стандартом де-факто для современных веб-приложений и микросервисов является использование JWT (JSON Web Tokens).

    Аутентификация vs Авторизация

    Прежде чем писать код, важно четко разделить два понятия, которые часто путают:

  • Аутентификация (Authentication): Ответ на вопрос «Кто ты?». Это процесс проверки личности пользователя (обычно через логин и пароль).
  • Авторизация (Authorization): Ответ на вопрос «Что тебе можно?». Это проверка прав доступа уже аутентифицированного пользователя к определенному ресурсу (например, только администратор может удалять товары).
  • Почему JWT?

    В классических веб-приложениях использовались сессии (Session Cookies). Сервер хранил информацию о залогиненном пользователе в своей памяти. Это отлично работало для монолитов, но плохо подходит для современных распределенных систем.

    JWT предлагает stateless (безгражданский) подход. Сервер не хранит сессию. Вся информация о пользователе зашифрована и подписана внутри самого токена, который клиент хранит у себя и присылает с каждым запросом.

    Структура токена

    JWT — это строка, состоящая из трех частей, разделенных точкой:

    Header.Payload.Signature

    !Визуальное представление трех составных частей JSON Web Token.

  • Header (Заголовок): Содержит тип токена (JWT) и алгоритм шифрования (обычно HMAC SHA256).
  • Payload (Полезная нагрузка): Содержит Claims (утверждения) — данные о пользователе (ID, имя, роль, срок действия токена). Эти данные не зашифрованы, а просто закодированы в Base64, поэтому никогда не передавайте там пароли.
  • Signature (Подпись): Гарантирует, что токен не был изменен. Она создается по следующей формуле:
  • Где: * — итоговая цифровая подпись (Signature). * — криптографический алгоритм хеширования. * — закодированный заголовок. * — закодированная полезная нагрузка. * — секретный ключ, который знает только сервер.

    Если злоумышленник изменит Payload (например, сделает себя админом), подпись перестанет совпадать, и сервер отвергнет токен.

    Настройка ASP.NET Core

    Для работы с JWT нам понадобится пакет Microsoft.AspNetCore.Authentication.JwtBearer. Установите его через терминал:

    1. Конфигурация (appsettings.json)

    Нам нужно придумать секретный ключ. Он должен быть длинным и сложным (минимум 32 символа для алгоритма HS256).

    2. Регистрация сервисов (Program.cs)

    В файле Program.cs мы должны сообщить приложению, что будем использовать JWT Bearer аутентификацию.

    3. Подключение Middleware

    Порядок здесь критически важен. Сначала мы должны понять, кто пришел (Authentication), и только потом проверять права (Authorization).

    Генерация токена

    Теперь создадим сервис, который будет выдавать токены при успешном входе. Обычно это делается в методе Login.

    Когда пользователь отправляет правильный логин и пароль, мы вызываем этот метод и возвращаем строку токена клиенту.

    Защита эндпоинтов

    Теперь, когда у нас настроена система, мы можем закрывать доступ к контроллерам или отдельным методам с помощью атрибута [Authorize].

    Простая защита

    Если неавторизованный пользователь попытается вызвать этот метод, он получит статус 401 Unauthorized.

    Ролевая модель (RBAC)

    Часто нам нужно разграничить права обычных пользователей и администраторов. В JWT мы добавили claim Role. Теперь мы можем использовать его:

    Если пользователь авторизован, но не имеет роли Admin, он получит статус 403 Forbidden.

    Minimal API и безопасность

    В Minimal API (о котором мы говорили в прошлой статье) атрибуты работают немного иначе, но принцип тот же. Мы используем методы расширения RequireAuthorization.

    Для использования политик их нужно предварительно зарегистрировать:

    Безопасность и лучшие практики

    Внедрение JWT — это мощный инструмент, но с ним нужно обращаться осторожно.

  • HTTPS обязателен: Токен передается в заголовке Authorization: Bearer <token>. Если вы используете HTTP, токен может быть перехвачен, и злоумышленник получит полный доступ к аккаунту.
  • Короткое время жизни: Делайте токены короткоживущими (например, 15-60 минут). Если токен украдут, злоумышленник сможет пользоваться им недолго.
  • Refresh Tokens: Чтобы пользователю не приходилось вводить пароль каждый час, реализуют механизм Refresh Token (токен обновления). Это долгоживущий токен, который хранится в БД и используется только для получения новой пары Access/Refresh токенов. Если Refresh Token скомпрометирован, его можно отозвать (удалить из БД).
  • Не храните секреты в коде: В примере выше ключ был в appsettings.json. В продакшене используйте переменные окружения или специальные хранилища секретов (Azure Key Vault, HashiCorp Vault).
  • Заключение

    Мы реализовали надежную систему защиты нашего API. Теперь:

    * Сервер умеет проверять подлинность пользователей через JWT. * Мы настроили Middleware для обработки токенов. * Мы научились защищать методы с помощью атрибута [Authorize] и ролей.

    Это завершает базовую настройку функциональности нашего бэкенда. У нас есть архитектура, база данных, API и безопасность. В следующих статьях мы займемся оптимизацией производительности и подключим кэширование с помощью Redis, чтобы наше приложение летало даже под высокой нагрузкой.

    5. Производительность и инфраструктура: Кэширование с Redis, Docker и фоновые задачи

    Производительность и инфраструктура: Кэширование с Redis, Docker и фоновые задачи

    В предыдущих статьях мы прошли большой путь: спроектировали архитектуру, подключили базу данных PostgreSQL, создали REST API и защитили его с помощью JWT. Наше приложение работает, оно безопасно и следует принципам чистого кода. Но готово ли оно к реальному миру?

    Представьте, что ваше приложение становится популярным. Тысячи пользователей одновременно запрашивают список товаров, фильтруют каталог и оформляют заказы. База данных начинает «захлебываться», время ответа растет, а сервер перегревается. Кроме того, возникает вопрос: как развернуть это приложение на сервере так, чтобы оно работало идентично тому, как оно работает на вашем локальном компьютере?

    В этой статье мы займемся производительностью и инфраструктурой. Мы внедрим кэширование с помощью Redis, упакуем приложение в Docker-контейнер и научимся выполнять тяжелые задачи в фоновом режиме.

    Кэширование данных: Ускоряем приложение

    Самая медленная операция в веб-приложении — это, как правило, поход в базу данных (I/O операция). Чтение с диска всегда медленнее, чем чтение из оперативной памяти.

    Чтобы оценить выигрыш от кэширования, рассмотрим упрощенную формулу времени обработки запроса:

    Где: * — общее время, которое пользователь ждет ответа. * — время передачи данных по сети. * — время выполнения кода приложения (бизнес-логика). * — время выполнения запроса в базе данных.

    Обычно занимает 80-90% времени обработки. Кэширование позволяет нам сократить практически до нуля для часто запрашиваемых данных, заменяя его на сверхбыстрое чтение из памяти ().

    Что такое Redis?

    Redis (Remote Dictionary Server) — это высокопроизводительное хранилище данных типа «ключ-значение», работающее в оперативной памяти. В отличие от PostgreSQL, который хранит данные на диске, Redis держит всё в RAM, обеспечивая отклик за микросекунды.

    В ASP.NET Core мы используем Redis как распределенный кэш (Distributed Cache). Это означает, что кэш находится в отдельном процессе (или на отдельном сервере), и если мы запустим 10 экземпляров нашего API, они все будут видеть одни и те же кэшированные данные.

    !Визуализация паттерна Cache-Aside: проверка кэша перед запросом к основной базе данных.

    Подключение Redis в ASP.NET Core

    Для начала нам понадобится пакет:

    Затем зарегистрируем сервис в Program.cs:

    И добавим строку подключения в appsettings.json (подразумевая, что Redis запущен локально):

    Использование IDistributedCache

    ASP.NET Core предоставляет удобную абстракцию IDistributedCache. Нам не нужно зависеть напрямую от библиотек Redis. Давайте реализуем паттерн Cache-Aside (Отложенная загрузка) в сервисе продуктов.

    Теперь повторные запросы одного и того же продукта не будут нагружать PostgreSQL.

    Docker: Контейнеризация приложения

    Частая проблема разработки: «На моем компьютере всё работает, а на сервере падает». Это происходит из-за разницы в окружении: разные версии .NET, разные настройки ОС, отсутствующие библиотеки.

    Docker решает эту проблему, упаковывая приложение и все его зависимости в контейнер. Контейнер — это изолированная среда, которая гарантированно работает одинаково везде.

    !Схематичное сравнение архитектуры виртуальных машин и Docker-контейнеров, демонстрирующее отсутствие лишних слоев в контейнерах.

    Создание Dockerfile

    Чтобы «докеризировать» наше ASP.NET Core приложение, нужно создать файл с именем Dockerfile в корне проекта. Это инструкция по сборке образа.

    Этот файл использует Multi-stage build (многоэтапную сборку). Сначала мы используем тяжелый образ sdk для компиляции кода, а затем копируем результат в легкий образ aspnet для запуска. Это делает итоговый контейнер очень маленьким.

    Оркестрация с Docker Compose

    Нашему приложению нужны PostgreSQL и Redis. Запускать их вручную неудобно. Docker Compose позволяет описать всю инфраструктуру в одном файле docker-compose.yml.

    Теперь одной командой docker-compose up мы поднимаем всё окружение: базу, кэш и наше приложение, уже настроенные на взаимодействие друг с другом.

    Фоновые задачи (Background Tasks)

    Иногда в ответ на действие пользователя нужно выполнить тяжелую работу: отправить email, сгенерировать отчет в PDF или обработать загруженное видео. Если делать это прямо внутри контроллера, пользователь будет смотреть на «крутящееся колесико» загрузки несколько секунд или минут.

    Правильный подход — вернуть ответ «Принято в обработку» (HTTP 202 Accepted) мгновенно, а задачу выполнить в фоне.

    IHostedService и BackgroundService

    ASP.NET Core имеет встроенную поддержку фоновых задач через интерфейс IHostedService. Для удобства существует абстрактный класс BackgroundService, который берет на себя управление запуском и остановкой.

    Создадим сервис, который раз в час очищает старые логи или неактивные корзины:

    Не забудьте зарегистрировать его в Program.cs:

    Очереди задач

    Для более сложных сценариев (например, отправка письма конкретному пользователю сразу после регистрации) Task.Delay не подходит. Здесь используются очереди задач.

    Простейшая реализация — Channel<T> (встроенная в .NET потокобезопасная очередь). Вы пишете задачу в канал из контроллера, а BackgroundService читает из канала и выполняет её. В крупных системах для этого используют внешние брокеры сообщений, такие как RabbitMQ или Kafka, но это тема для отдельного разговора.

    Заключение

    Сегодня мы превратили наше приложение из простого API в профессиональную систему:

  • Redis ускорил чтение данных, сняв нагрузку с базы данных.
  • Docker и Docker Compose позволили нам упаковать приложение и разворачивать его одной командой в любом окружении.
  • BackgroundService позволил вынести тяжелые операции в фон, улучшив отзывчивость интерфейса для пользователя.
  • Эти три компонента являются стандартом индустрии. Владение ими — обязательное требование для Middle Backend разработчика.

    На этом наш курс подходит к концу. Мы прошли путь от File -> New Project до масштабируемого, безопасного и производительного приложения. Дальнейшее развитие зависит от вас: изучайте микросервисы, CI/CD пайплайны и облачные провайдеры (Azure, AWS). Удачи в коде!