C# Backend-разработчик: Путь от стажера до Middle

Комплексный курс для повышения квалификации .NET разработчиков, охватывающий переход от базовых знаний к профессиональной разработке сложных систем. Программа включает углубленное изучение платформы, работу с данными, архитектурные паттерны и современные инструменты деплоя.

1. Фундамент .NET: Управление памятью, асинхронное программирование и продвинутые возможности C#

Фундамент .NET: Управление памятью, асинхронное программирование и продвинутые возможности C#

Добро пожаловать на курс «C# Backend-разработчик: Путь от стажера до Middle». Переход от уровня Junior к Middle — это не просто накопление лет опыта, это качественное изменение в понимании того, как работает ваш код. Стажер пишет код, который работает. Middle пишет код, который работает эффективно, предсказуемо и масштабируемо.

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

Управление памятью: Стек и Куча

Понимание того, где и как хранятся данные, критически важно для предотвращения утечек памяти и лишней нагрузки на процессор. В .NET память делится на две основные области: Стек (Stack) и Куча (Heap).

!Визуализация различий хранения данных в Стеке и Куче

Стек (Stack)

Это область памяти, работающая по принципу LIFO (Last In, First Out — последним пришел, первым ушел). Стек очень быстрый, так как выделение памяти происходит простым смещением указателя.

* Что здесь хранится: Значимые типы (int, double, bool, struct), параметры методов и локальные переменные (если они не являются частью замыкания). * Жизненный цикл: Переменные живут только пока выполняется метод. Как только метод завершается, память освобождается автоматически.

Куча (Heap)

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

* Что здесь хранится: Ссылочные типы (class, interface, delegate, string, массивы). * Жизненный цикл: Управляется сборщиком мусора (Garbage Collector).

Boxing и Unboxing

Одной из частых причин проблем с производительностью является Boxing (упаковка) — процесс преобразования значимого типа в ссылочный (перенос из стека в кучу). Обратный процесс называется Unboxing (распаковка).

> Избегайте лишнего боксинга в горячих путях кода (циклах, часто вызываемых методах), так как это создает нагрузку на GC.

Сборщик мусора (Garbage Collector)

В отличие от C++, в C# вам не нужно удалять объекты вручную. Этим занимается Garbage Collector (GC). Однако, чтобы писать эффективный код, нужно понимать алгоритм его работы.

GC в .NET основан на поколениях (Generations). Это сделано для оптимизации: доказано, что большинство объектов «умирают» молодыми.

  • Поколение 0 (Gen 0): Сюда попадают все новые объекты. Сборка мусора здесь происходит очень часто и быстро. Если объект выживает после сборки, он переходит в Gen 1.
  • Поколение 1 (Gen 1): Буфер между короткоживущими и долгоживущими объектами.
  • Поколение 2 (Gen 2): Здесь живут «старички» — статические данные, кэши, синглтоны. Сборка мусора здесь («Full GC») самая дорогая и может заморозить приложение на заметное время.
  • Проблема больших объектов (LOH)

    Объекты размером более 85 000 байт попадают в специальную кучу — Large Object Heap (LOH). Она собирается только во время сборки 2-го поколения и по умолчанию не дефрагментируется (хотя в новых версиях .NET это поведение можно настроить).

    Асинхронное программирование: async/await

    Асинхронность — это не про то, чтобы делать всё одновременно (параллелизм). Это про то, чтобы не блокировать поток, пока мы ждем завершения операции (обычно ввода-вывода: запрос к БД, чтение файла, HTTP-запрос).

    Как это работает под капотом

    Когда вы пишете ключевые слова async и await, компилятор C# разворачивает ваш метод в сложную структуру — Конечный автомат (State Machine).

    !Схематичное изображение освобождения потока при использовании await

  • Метод запускается синхронно до первого await.
  • Если операция в await не завершена, метод возвращает управление вызывающему коду (возвращает Task).
  • Текущий поток освобождается и возвращается в ThreadPool.
  • Когда операция завершается, продолжение метода (код после await) ставится в очередь на выполнение (возможно, уже в другом потоке).
  • Распространенные ошибки

    * Async Void: Никогда не используйте async void, кроме обработчиков событий (Event Handlers). В случае исключения в async void методе, процесс приложения упадет (crash), и вы не сможете это перехватить через try-catch. * Блокировка через .Result или .Wait(): Это приводит к синхронной блокировке потока и может вызвать Deadlock (взаимную блокировку), особенно в контекстах с синхронизацией (ASP.NET, WPF).

    Продвинутые возможности: Span<T> и Memory<T>

    С выходом новых версий .NET (Core 2.1+) появились инструменты для работы с памятью без лишних аллокаций. Это критически важно для высоконагруженных backend-систем.

    Span<T>

    Span<T> — это структура, которая позволяет работать с непрерывным участком памяти (массив, стек, неуправляемая память) без копирования данных.

    Представьте, что вам нужно взять подстроку. В классическом C# substring создает новую строку (аллокация в куче).

    С использованием Span:

    Это позволяет писать парсеры и обработчики данных с нулевой нагрузкой на GC.

    Оценка сложности алгоритмов

    Backend-разработчик уровня Middle должен понимать, как выбор коллекции влияет на производительность. Для этого используется нотация «O большое».

    Например, поиск элемента в списке List<T> имеет сложность:

    Где — функция, описывающая скорость роста времени выполнения, а — количество элементов в списке. Это означает, что в худшем случае нам придется перебрать все элементы.

    В то же время, поиск по ключу в словаре Dictionary<TKey, TValue> стремится к:

    Где означает константное время, не зависящее от количества элементов (при отсутствии коллизий хеширования).

    Заключение

    Мы рассмотрели фундамент, на котором строится эффективная разработка на C#. Понимание работы памяти (Стек vs Куча), механизма GC и конечного автомата async/await отличает профессионала от новичка. В следующих статьях мы углубимся в архитектуру приложений и работу с базами данных.

    Теперь перейдем к проверке знаний.

    2. Разработка веб-API на ASP.NET Core: Middleware, аутентификация и внедрение зависимостей

    Разработка веб-API на ASP.NET Core: Middleware, аутентификация и внедрение зависимостей

    В предыдущей статье мы заложили фундамент, разобравшись с управлением памятью и асинхронностью в C#. Теперь, когда мы понимаем, как писать эффективный код, пришло время применить эти знания для создания того, чем занимается большинство Backend-разработчиков — веб-сервисов.

    ASP.NET Core — это не просто фреймворк, это высокопроизводительная кроссплатформенная платформа. Чтобы перейти от уровня Junior к Middle, недостаточно просто создать контроллер и вернуть JSON. Необходимо понимать архитектуру обработки запроса: как приложение собирается из кирпичиков (Dependency Injection), как запрос проходит сквозь систему (Middleware) и как мы понимаем, кто к нам стучится (Authentication).

    Внедрение зависимостей (Dependency Injection)

    В старых ASP.NET приложениях разработчики часто создавали экземпляры классов вручную, используя оператор new. Это приводило к жесткой связности кода (Tight Coupling). Если классу OrderService нужен был DatabaseRepository, он создавал его внутри себя. Это делало код трудным для тестирования и поддержки.

    ASP.NET Core был спроектирован с учетом паттерна Dependency Injection (DI) с самого начала. Это означает, что объекты не создают свои зависимости, а запрашивают их.

    Контейнер DI

    В центре всего находится IServiceCollection. Это список инструкций для приложения: «если кто-то попросит IUserRepository, дай ему вот этот класс SqlUserRepository».

    Самое важное для Middle-разработчика — понимать жизненный цикл (Lifetime) сервисов. Ошибка здесь может привести к утечкам памяти или некорректной работе с данными.

    !Визуализация жизненных циклов зависимостей: Transient, Scoped и Singleton

    1. Transient (Временный)

    Сервис создается каждый раз, когда он запрашивается. Даже в рамках одного HTTP-запроса, если два разных класса попросят этот сервис, они получат два разных экземпляра.

    * Когда использовать: Легковесные сервисы без состояния (stateless). Например, сервис для отправки email или калькулятор цен.

    2. Scoped (В рамках запроса)

    Сервис создается один раз на один клиентский запрос (HTTP Request). Все компоненты, участвующие в обработке этого конкретного запроса, получат один и тот же экземпляр.

    * Когда использовать: Это самый частый выбор для веб-разработки. Контекст базы данных (Entity Framework DbContext) регистрируется именно так. Это гарантирует, что все операции в рамках одного запроса к API выполняются в одной транзакции.

    3. Singleton (Одиночка)

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

    * Когда использовать: Кэширование, конфигурационные сервисы, синглтоны по дизайну. * Опасность: Будьте осторожны с потокобезопасностью! Так как один экземпляр используется параллельно разными потоками, изменение состояния внутри Singleton без блокировок (lock) приведет к гонке данных.

    > Никогда не внедряйте Scoped-сервис внутрь Singleton-сервиса. Это приведет к «Captive Dependency» (захваченной зависимости), когда Scoped-сервис не сможет освободиться и станет жить вечно, как Singleton.

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

    Когда запрос приходит на ваш сервер, он не попадает сразу в метод контроллера. Он проходит через серию компонентов, называемых Middleware (промежуточное ПО).

    Представьте себе водопроводную трубу с системой фильтров. Вода (запрос) проходит через фильтр грубой очистки, затем через угольный фильтр, и только потом попадает в кран. В ASP.NET Core вы сами выстраиваете эту трубу в файле Program.cs.

    !Схема прохождения HTTP-запроса и ответа через конвейер Middleware

    Как это работает в коде

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

    В отличие от регистрации сервисов в DI, порядок добавления Middleware строго определен.

  • Если вы поставите UseAuthorization перед UseAuthentication, приложение попытается проверить права доступа у пользователя, которого оно еще не опознало.
  • Если UseExceptionHandler будет в конце, он не сможет перехватить ошибки, возникшие в предыдущих слоях.
  • Каждый Middleware имеет выбор: * Передать запрос следующему компоненту (await next()). * Прервать цепочку и вернуть ответ (Short-circuiting). Например, если статический файл найден, нет смысла запускать контроллеры.

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

    Эти два понятия часто путают, но для backend-разработчика разница фундаментальна.

    * Аутентификация (Authentication): Проверка подлинности. Ответ на вопрос «Кто ты?». (Пример: проверка логина и пароля, валидация токена). * Авторизация (Authorization): Проверка прав. Ответ на вопрос «Можешь ли ты это сделать?». (Пример: есть ли у пользователя роль Admin).

    JWT (JSON Web Token)

    В современном мире REST API стандартом де-факто является использование JWT. Это stateless-подход: сервер не хранит сессию пользователя в памяти. Вся информация о пользователе зашифрована в самом токене, который клиент присылает в заголовке Authorization.

    Структура JWT:

  • Header: Алгоритм шифрования.
  • Payload: Полезная нагрузка (Claims). Здесь хранятся ID пользователя, его роль, email.
  • Signature: Подпись, гарантирующая, что токен не был изменен.
  • В ASP.NET Core работа с токенами настраивается через JwtBearer:

    После успешной валидации токена, ASP.NET Core создает объект ClaimsPrincipal и помещает его в HttpContext.User. Именно оттуда вы в контроллере можете узнать User.Identity.Name или проверить роль.

    Внедрение зависимостей в Action-методы

    Благодаря атрибуту [FromServices], вы можете внедрять зависимости прямо в метод контроллера, если они нужны только там, а не во всем классе. Однако, стандартной практикой является внедрение через конструктор (Constructor Injection).

    Такой подход делает код чистым и явно объявляет, что нужно контроллеру для работы.

    Заключение

    Мы разобрали «скелет» приложения на ASP.NET Core. Понимание того, как работает DI-контейнер, спасает от множества багов, связанных с состоянием объектов. А знание устройства Middleware позволяет гибко настраивать обработку запросов, добавляя логирование, метрики или кастомную обработку ошибок в нужное место конвейера.

    В следующей статье мы погрузимся в работу с данными: Entity Framework Core, миграции и оптимизация SQL-запросов.

    Для более глубокого изучения тем рекомендую обратиться к официальной документации: * Промежуточное ПО (Middleware) в ASP.NET Core * Внедрение зависимостей в ASP.NET Core

    3. Работа с данными: Entity Framework Core, оптимизация SQL-запросов и миграции

    Работа с данными: Entity Framework Core, оптимизация SQL-запросов и миграции

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

    В мире .NET стандартом де-факто для работы с базами данных является Entity Framework Core (EF Core). Однако, именно на этом слое Junior-разработчики совершают ошибки, которые убивают производительность приложения при росте нагрузки. Стажер пишет запрос, который возвращает данные. Middle пишет запрос, который не «роняет» базу данных в «черную пятницу».

    Сегодня мы разберем, как работает EF Core, что такое проблема «N+1», почему IQueryable — это не IEnumerable, и как управлять изменениями схемы БД.

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

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

    Entity Framework Core позволяет вам работать с базой данных, используя C#, а не SQL. Вы манипулируете объектами, а фреймворк сам генерирует SQL-запросы.

    DbContext и DbSet

    Сердцем EF Core является класс, наследуемый от DbContext. Он реализует два важных паттерна:

  • Unit of Work (Единица работы): Отслеживает изменения в объектах и сохраняет их одной транзакцией при вызове SaveChanges().
  • Repository (Репозиторий): Свойства типа DbSet<T> представляют собой коллекции объектов, которые проецируются на таблицы БД.
  • > В контексте Dependency Injection, DbContext обычно регистрируется как Scoped. Это означает, что один экземпляр контекста создается на один HTTP-запрос.

    Миграции: Управление схемой БД

    В старых подходах (Database First) сначала создавалась база данных, а потом генерировался код. В современном мире чаще используется подход Code First: вы описываете классы C#, а EF Core создает по ним базу данных.

    Но что делать, если вы добавили новое свойство PhoneNumber в класс User? Базу нужно обновить. Для этого существуют миграции.

    !Визуализация процесса применения миграций как системы контроля версий для базы данных

    Миграция — это C# класс, который содержит две инструкции: * Up(): Как применить изменения (например, CREATE TABLE или ALTER TABLE). * Down(): Как откатить изменения (например, DROP TABLE).

    Золотые правила миграций для Middle-разработчика:

  • Никогда не меняйте файлы миграций вручную, если не понимаете последствий. Лучше создать новую миграцию.
  • Проверяйте сгенерированный SQL. Команда Script-Migration позволяет увидеть чистый SQL, который будет выполнен на сервере. Это последний рубеж проверки перед продакшеном.
  • Идемпотентность. В идеале скрипты миграций должны быть идемпотентными (их можно запускать много раз, и результат не изменится), хотя EF Core берет часть этой работы на себя через таблицу __EFMigrationsHistory.
  • Проблема N+1: Главный враг производительности

    Это самая распространенная ошибка при работе с ORM. Она возникает, когда приложение делает один запрос для получения списка объектов, а затем для каждого объекта делает дополнительный запрос, чтобы подгрузить связанные данные.

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

    Плохой код (Lazy Loading / Явная загрузка в цикле):

    csharp modelBuilder.Entity<User>() .HasIndex(u => u.Email) .IsUnique(); ``

    Заключение

    Работа с данными — это баланс между удобством разработки и производительностью. EF Core — мощный инструмент, но он требует понимания того, что происходит «под капотом».

    Мы изучили:

  • Как миграции помогают эволюционировать схеме БД.
  • Как избежать проблемы N+1 с помощью Include.
  • Почему важно фильтровать данные через IQueryable до материализации.
  • Как ускорить чтение с помощью AsNoTracking`.
  • В следующей статье мы перейдем к обеспечению качества нашего кода: модульному (Unit) и интеграционному тестированию.

    Для углубленного изучения рекомендую: * Официальная документация EF Core * Производительность EF Core

    4. Архитектура и качество: Принципы SOLID, Clean Architecture и написание модульных тестов

    Архитектура и качество: Принципы SOLID, Clean Architecture и написание модульных тестов

    В предыдущих статьях мы прошли путь от основ управления памятью до создания полноценного API с базой данных на Entity Framework Core. На данном этапе у вас есть работающее приложение. Но работает ли оно правильно с точки зрения архитектуры? Будет ли легко добавить новую фичу через полгода? Не сломается ли старый функционал при внесении изменений?

    Отличие Middle-разработчика от Junior заключается не только в знании синтаксиса, но и в умении писать поддерживаемый, тестируемый и гибкий код. Сегодня мы разберем три кита качественного Backend-приложения: принципы SOLID, Чистую Архитектуру и модульное тестирование.

    Принципы SOLID

    SOLID — это акроним пяти принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином (Дядя Боб). Это не просто правила, это ментальная модель для борьбы со сложностью кода.

    S — Single Responsibility Principle (Принцип единственной ответственности)

    > У класса должна быть только одна причина для изменения.

    Это не значит, что у класса должен быть только один метод. Это значит, что класс должен решать одну бизнес-задачу.

    Плохо: Класс OrderService рассчитывает сумму заказа, сохраняет его в БД и отправляет email клиенту. Если изменится логика расчета скидок — мы меняем класс. Если сменится провайдер email-рассылок — мы снова меняем этот же класс.

    Хорошо: * PriceCalculator — считает сумму. * OrderRepository — сохраняет в БД. * EmailNotificationService — отправляет письма. * OrderService — просто координирует работу этих компонентов.

    O — Open/Closed Principle (Принцип открытости/закрытости)

    > Программные сущности должны быть открыты для расширения, но закрыты для модификации.

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

    Представьте, что вы рассчитываете налог. Вместо if (country == "USA") { ... } else if (country == "UK") { ... }, вы создаете интерфейс ITaxCalculator и разные реализации для каждой страны. При добавлении новой страны вы просто добавляете новый класс, не трогая старый код.

    L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

    > Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.

    Если Bird имеет метод Fly(), а вы создаете наследника Penguin, который не умеет летать и выбрасывает исключение в методе Fly() — вы нарушаете LSP. Наследник не должен ломать ожидания, заложенные в базовом классе.

    I — Interface Segregation Principle (Принцип разделения интерфейса)

    > Клиенты не должны зависеть от методов, которые они не используют.

    Лучше создать три узкоспециализированных интерфейса, чем один «божественный» интерфейс общего назначения.

    Пример: Вместо IUserAction (Login, Register, Delete, Ban, PromoteToAdmin), лучше сделать IUserAuthentication (Login, Register) и IUserAdministration (Ban, PromoteToAdmin). Обычному пользователю не нужны методы админа.

    D — Dependency Inversion Principle (Принцип инверсии зависимостей)

    > Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

    Именно этот принцип лежит в основе Dependency Injection, который мы разбирали во второй статье. Ваша бизнес-логика (верхний уровень) не должна зависеть от конкретного DbContext (нижний уровень). Она должна зависеть от интерфейса IRepository.

    Clean Architecture (Чистая Архитектура)

    Принципы SOLID говорят нам, как писать классы. Чистая Архитектура говорит, где их располагать.

    Главная идея Чистой Архитектуры — разделение ответственности по слоям и строгое правило зависимостей: зависимости могут указывать только внутрь.

    !Диаграмма слоев Чистой Архитектуры, показывающая направление зависимостей от внешних слоев к внутренним.

    1. Domain (Домен)

    Центр вашего приложения. Здесь живут Сущности (Entities) и бизнес-правила, которые не зависят ни от чего внешнего. Здесь нет ссылок на Entity Framework, HTTP или JSON.

    2. Application (Приложение)

    Здесь находятся сценарии использования (Use Cases). Этот слой управляет потоком данных. Он знает о Домене, но не знает о Базе Данных или Вебе. Здесь живут интерфейсы (например, IOrderRepository), но не их реализации.

    3. Infrastructure (Инфраструктура)

    Здесь реализуются интерфейсы из слоя Application. Здесь подключается Entity Framework Core, здесь отправляются запросы к сторонним API, здесь происходит чтение файлов. Этот слой знает о слое Application.

    4. Presentation (Представление)

    Это ваш API (Controllers, Middleware). Он зависит от Application. Его задача — принять HTTP-запрос, превратить его в понятный для Application объект (DTO) и вернуть результат.

    Такое разделение позволяет вам, например, заменить SQL Server на MongoDB, переписав только слой Infrastructure, не трогая бизнес-логику.

    Модульное тестирование (Unit Testing)

    Как убедиться, что ваша сложная архитектура работает правильно? Ручное тестирование через Postman — это долго и ненадежно. Middle-разработчик пишет автотесты.

    Unit-тест (Модульный тест) — это код, который проверяет минимальную единицу логики (обычно метод) в изоляции от внешних зависимостей.

    Инструменты

    В мире .NET стандартом являются: * xUnit — фреймворк для запуска тестов. * Moq или NSubstitute — библиотеки для создания заглушек (Mock-объектов).

    Паттерн AAA

    Любой тест строится по структуре:
  • Arrange (Подготовка): Создаем объекты, настраиваем данные.
  • Act (Действие): Вызываем тестируемый метод.
  • Assert (Проверка): Сравниваем полученный результат с ожидаемым.
  • Пример тестирования сервиса

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

    Чтобы протестировать этот метод, нам не нужна реальная база данных. Мы используем Mock.

    Метрика покрытия кода (Code Coverage)

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

    Где — процент покрытия, — количество строк кода, выполненных во время тестов, а — общее количество строк кода в проекте.

    Хотя 100% покрытие — это идеал, на практике хорошим показателем для Middle-разработчика считается 70-80%. Главное — покрывать тестами сложную бизнес-логику, а не простые геттеры и сеттеры.

    Заключение

    Переход к Middle-уровню требует смены мышления. Вы перестаете писать код «в лоб» и начинаете проектировать систему.

  • SOLID помогает создавать гибкие классы.
  • Clean Architecture помогает структурировать проект так, чтобы он не превратился в спагетти.
  • Unit-тесты дают уверенность в том, что ваш рефакторинг ничего не сломал.
  • Внедрение этих практик замедляет разработку на старте, но многократно ускоряет ее на длинной дистанции. Это и есть профессионализм.

    5. Highload и инфраструктура: Кэширование, брокеры сообщений, Docker и основы микросервисов

    Highload и инфраструктура: Кэширование, брокеры сообщений, Docker и основы микросервисов

    Мы прошли долгий путь: от изучения того, как работает память в .NET, до построения чистой архитектуры с модульными тестами. Ваше приложение работает правильно, код чист, а бизнес-логика покрыта тестами. Но готово ли ваше приложение к реальному миру?

    Что произойдет, если завтра на ваш сервис зайдет не 10 человек, а 10 000? Что если база данных начнет «захлебываться» от запросов? Как развернуть приложение на сервере так, чтобы оно работало идентично вашей локальной машине?

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

    Кэширование: Ускоряем чтение данных

    Самым узким местом в большинстве веб-приложений является база данных. Дисковые операции всегда медленнее, чем операции в оперативной памяти. Чтобы снизить нагрузку на БД и ускорить ответы клиенту, мы используем кэширование.

    In-Memory vs Distributed Cache

  • In-Memory Cache (IMemoryCache): Данные хранятся в оперативной памяти самого веб-сервера. Это самый быстрый способ, но он имеет минусы: если у вас несколько экземпляров сервиса (реплик), кэш у них будет разный (рассинхронизация). При перезагрузке сервера кэш пропадает.
  • Distributed Cache (IDistributedCache): Данные хранятся во внешнем хранилище (обычно Redis). Это позволяет всем экземплярам вашего сервиса видеть одни и те же данные. Данные переживают перезагрузку приложения.
  • Паттерн Cache-Aside

    Это наиболее распространенный сценарий использования кэша для Middle-разработчика.

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

    Реализация на C#

    Для работы с Redis в .NET чаще всего используют библиотеку StackExchange.Redis, но Microsoft предоставляет удобную абстракцию IDistributedCache.

    ```csharp public async Task<Product> GetProductAsync(int id) { string key = T_{avg}HT_{cache}T_{db}H = 0.9SP = \{C, A, PT\}|S|SP$ — множество всех свойств (Consistency, Availability, Partition Tolerance).

    Поскольку в распределенных системах сеть ненадежна, Partition Tolerance (P) является обязательным условием. Поэтому на практике мы выбираем между CP (согласованность + устойчивость, но возможны отказы в обслуживании) и AP (доступность + устойчивость, но данные могут быть устаревшими).

    > Микросервисы — это не серебряная пуля. Это способ решения организационных проблем ценой технической сложности.

    Заключение

    Переход от Junior к Middle — это понимание того, что код не живет в вакууме.

  • Мы используем Redis, чтобы снизить нагрузку на базу данных.
  • Мы используем RabbitMQ, чтобы сделать процессы асинхронными и надежными.
  • Мы используем Docker, чтобы гарантировать предсказуемость развертывания.
  • Эти инструменты формируют инфраструктурный скелет современного Backend-разработчика. В следующей части курса мы поговорим о CI/CD и культуре DevOps.

    Для углубленного изучения: * Кэширование в .NET * Контейнеризация приложений .NET * Паттерны микросервисов