C# от Junior до Senior: полный путь разработчика

Курс последовательно проводит от базового синтаксиса и ООП в C# до продвинутых практик проектирования, производительности и архитектуры. Вы освоите современные возможности .NET, работу с данными, тестирование, многопоточность и принципы построения надежных систем на уровне Senior.

1. Основы C# и .NET: синтаксис, типы, коллекции

Основы C# и .NET: синтаксис, типы, коллекции

Зачем разработчику понимать основы .NET

C# — язык, который работает внутри экосистемы .NET. Чтобы уверенно расти от Junior к Senior, важно понимать не только «как написать код», но и где он выполняется, какие типы данных вы используете и как храните/обрабатываете наборы данных.

В этой статье разберём:

  • как устроены C# и .NET на базовом уровне
  • базовый синтаксис и структуру программы
  • типы данных и ключевые отличия значимых и ссылочных типов
  • самые важные коллекции и когда их выбирать
  • Полезные источники (официальная документация):

  • .NET — обзор
  • C# — руководство
  • Коллекции в .NET
  • Обобщения (generics)
  • Как исполняется C# код: .NET, CLR и JIT

    Когда вы пишете код на C#, компилятор (Roslyn) превращает его не сразу в машинные инструкции, а в промежуточный язык IL (Intermediate Language). Затем среда выполнения .NET запускает этот IL.

    Ключевые понятия:

  • CLR (Common Language Runtime) — среда выполнения .NET: управляет памятью, исключениями, потоками, безопасностью.
  • JIT (Just-In-Time) — компилирует IL в машинный код во время выполнения.
  • GC (Garbage Collector) — сборщик мусора: автоматически освобождает память, занятую больше не используемыми объектами.
  • Практический вывод:

  • вы обычно не освобождаете память вручную, но должны понимать, что частое создание объектов и лишние аллокации могут влиять на производительность
  • ошибки времени выполнения (например, NullReferenceException) — часть реальности, и с ними нужно уметь работать
  • Минимальная структура программы на C#

    В современных версиях C# можно писать top-level statements — код без явного class Program и static void Main().

    Что здесь происходит:

  • using System; подключает пространство имён, где лежит Console
  • Console.WriteLine(...) вызывает метод печати строки в консоль
  • Если нужен классический вариант:

    Синтаксис: переменные, выражения, операторы

    Переменные и присваивание

    Переменная имеет тип.

    Иногда удобно использовать varне динамическую типизацию, а вывод типа компилятором:

    Правило: используйте var, когда тип очевиден из правой части, иначе — пишите тип явно.

    Базовые операторы

  • арифметика: +, -, *, /, %
  • сравнение: ==, !=, <, >, <=, >=
  • логика: &&, ||, !
  • присваивание: =, +=, -=, *=, /=
  • Для строк оператор + выполняет конкатенацию:

    Но для большого количества склеиваний обычно лучше StringBuilder (к этому вернёмся в следующих темах про производительность).

    Управляющие конструкции: ветвления и циклы

    if / else

    switch

    Удобен для множественного выбора.

    Современный C# также поддерживает switch-выражения, но их лучше разбирать, когда базовый switch уже понятен.

    Циклы for, while, foreach

    foreach работает с любым объектом, который можно перечислять (обычно это IEnumerable<T>).

    Методы: параметры, возвращаемые значения, перегрузка

    Методы позволяют переиспользовать код и структурировать логику.

    Полезные моменты:

  • метод может возвращать void, если ничего не возвращает
  • методы могут быть перегружены (одно имя, разные параметры)
  • Типы в C#: значимые и ссылочные

    Типы в C# делятся на две большие категории:

  • значимые типы (value types): хранят значение «целиком»
  • ссылочные типы (reference types): переменная хранит ссылку на объект в памяти
  • К значимым типам относятся:

  • числовые типы (int, long, double, decimal)
  • bool, char
  • struct, enum
  • К ссылочным типам относятся:

  • string
  • class
  • массивы (int[], string[])
  • большинство коллекций (List<T>, Dictionary<TKey, TValue>)
  • !Схема различий между значимыми и ссылочными типами и где они обычно располагаются в памяти

    Практический смысл различия:

  • копирование значимого типа копирует значение
  • копирование ссылочного типа копирует ссылку (две переменные могут указывать на один объект)
  • Пример:

    Встроенные типы и частые нюансы

    Числа: int vs long vs decimal

  • int — самый частый выбор для целых чисел
  • long — когда диапазона int недостаточно
  • decimal — чаще для финансов и расчётов, где важна точность десятичной арифметики
  • string — ссылочный тип, но ведёт себя «как значение»

    string неизменяем (immutable). Любое «изменение строки» создаёт новую строку.

    null и nullable-типы

    Ссылочные типы могут быть null (то есть «ни на что не указывают»).

    Для значимых типов есть Nullable<T> (синтаксис T?):

    Чтобы безопасно работать с nullable:

  • проверяйте null через if (x is null)
  • используйте оператор объединения с null: value ?? defaultValue
  • Документация: Nullable value types

    Массивы: базовая коллекция фиксированного размера

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

    Когда массив хорош:

  • размер известен заранее
  • нужен быстрый доступ по индексу
  • Когда лучше коллекции:

  • размер меняется
  • нужны удобные операции (поиск, добавление, удаление)
  • Коллекции в .NET: что выбрать и почему

    Почти всегда вы будете работать с коллекциями из System.Collections.Generic.

    List<T> — динамический массив

    Самая популярная коллекция.

    Когда выбирать List<T>:

  • нужен порядок элементов
  • нужен доступ по индексу
  • добавления в конец — частая операция
  • Dictionary<TKey, TValue> — ключ-значение

    Подходит для быстрого доступа по ключу.

    Рекомендации:

  • проверяйте наличие ключа через ContainsKey или TryGetValue
  • HashSet<T> — уникальные элементы

    Используется, когда важна уникальность.

    Queue<T> и Stack<T>

  • Queue<T> — очередь (FIFO: первым пришёл — первым вышел)
  • Stack<T> — стек (LIFO: последним пришёл — первым вышел)
  • Сравнение коллекций

    | Коллекция | Хранит порядок | Доступ по индексу | Поиск по ключу | Уникальность | Типичный сценарий | |---|---|---|---|---|---| | T[] | да | да | нет | нет | фиксированный размер, максимальная простота | | List<T> | да | да | нет | нет | список объектов, добавления/перебор | | Dictionary<TKey, TValue> | частично | нет | да | ключи уникальны | быстрый доступ по идентификатору | | HashSet<T> | нет | нет | нет | да | множество уникальных значений | | Queue<T> | да | нет | нет | нет | обработка задач по очереди | | Stack<T> | да | нет | нет | нет | откат, парсинг, обход в глубину |

    Интерфейсы перечисления: IEnumerable<T> и foreach

    Большая часть коллекций поддерживает IEnumerable<T>. Это означает, что коллекцию можно перебирать.

    Важно:

  • IEnumerable<T> — это контракт на перечисление, а не конкретная структура хранения
  • один и тот же интерфейс может быть реализован массивом, списком, результатом LINQ-запроса, генератором и т.д.
  • !Схема того, как данные проходят через IEnumerable и как foreach и LINQ работают поверх перечисления

    LINQ: базовое понимание без углубления

    LINQ позволяет описывать преобразования коллекций декларативно.

    Два ключевых факта для старта:

  • многие LINQ-операторы выполняются лениво: вычисления происходят при перечислении
  • LINQ часто возвращает IEnumerable<T>, а чтобы получить список, вызывают ToList()
  • Документация: LINQ (Language Integrated Query)

    Итоги

    После этой статьи у вас должна сложиться базовая картина:

  • C# код выполняется внутри .NET через CLR, JIT и GC
  • вы умеете писать простые программы, использовать переменные, операторы, ветвления и циклы
  • вы понимаете разницу между значимыми и ссылочными типами, а также роль null
  • вы знаете, чем отличаются массивы и основные коллекции (List, Dictionary, HashSet, Queue, Stack)
  • вы понимаете, что такое IEnumerable<T> и почему foreach и LINQ так распространены
  • Дальше на курсе эти основы станут фундаментом для тем про ООП, исключения, асинхронность, работу с памятью, архитектуру и производительность.

    2. ООП и современный C#: классы, интерфейсы, generics, LINQ

    ООП и современный C#: классы, интерфейсы, generics, LINQ

    Зачем ООП и «современный C#» после базовых типов и коллекций

    В прошлой теме вы разобрали типы, null, массивы и ключевые коллекции (List<T>, Dictionary<TKey, TValue>, IEnumerable<T>). Следующий шаг к уровню Middle и выше — научиться моделировать предметную область и строить расширяемый код.

    ООП в C# — это не «теория ради теории», а практичный способ:

  • описывать сущности бизнеса как типы (классы, записи)
  • отделять контракт от реализации (интерфейсы)
  • переиспользовать код без копипаста (обобщения)
  • выразительно обрабатывать коллекции (LINQ)
  • Официальная документация для темы:

  • Классы
  • Свойства
  • Интерфейсы
  • Обобщения
  • Ограничения параметров типа
  • LINQ
  • ООП в практическом смысле

    ООП обычно объясняют через четыре идеи.

  • Инкапсуляция: прячем детали реализации и защищаем состояние объекта от некорректного использования.
  • Абстракция: выделяем главное и оформляем в понятный интерфейс или публичные методы.
  • Наследование: переиспользуем общий код и общий контракт в иерархии типов.
  • Полиморфизм: работаем с объектом через общий тип, а конкретное поведение определяется реализацией.
  • Важно: в production-коде часто выигрывает композиция (сборка поведения из частей) по сравнению с глубоким наследованием.

    !Как интерфейс отделяет контракт уведомлений от конкретных реализаций

    Классы: состояние, поведение, доступ

    Класс описывает состояние (данные) и поведение (методы), которые работают с этими данными.

    Поля, свойства, методы

  • Поле обычно делают private, чтобы не позволять менять состояние напрямую.
  • Свойство (property) — управляемый доступ к значению (можно валидировать, логировать, ограничивать запись).
  • Метод — операция над объектом.
  • Пример с контролем инвариантов (правил состояния):

    Здесь класс гарантирует, что баланс не станет отрицательным и не будет «дыр» в логике.

    Модификаторы доступа

  • public — доступно всем.
  • private — доступно только внутри класса.
  • protected — доступно в классе и наследниках.
  • internal — доступно в пределах сборки.
  • Практическое правило: начинайте с минимально возможного доступа и расширяйте только при необходимости.

    Свойства init и неизменяемые объекты

    Современный C# позволяет делать свойства, которые можно установить только при создании объекта.

    Это помогает строить более предсказуемые модели: меньше «частично заполненных» объектов.

    Record-типы: модели данных с равенством по значению

    Record удобно использовать для моделей данных, где важны:

  • неизменяемость
  • структурное сравнение (равенство по значениям)
  • краткая запись
  • Пример:

    with создаёт новый объект на основе старого, что хорошо сочетается с функциональным стилем и уменьшает количество побочных эффектов.

    Документация: Record-типы

    Наследование и полиморфизм

    Наследование позволяет сказать: «Dog — это частный случай Animal». Полиморфизм позволяет работать с Animal, не зная конкретный тип.

    Виртуальные методы: virtual и override

    Почему это важно:

  • OrderService зависит от контракта INotifier, а не от конкретного EmailNotifier
  • реализацию можно заменить без переписывания бизнес-логики
  • интерфейсы — фундамент для dependency injection и тестирования
  • Современная деталь: в C# возможны реализации по умолчанию в интерфейсах, но использовать их стоит осторожно и осознанно.

    Generics: обобщения для типобезопасности и переиспользования

    Обобщения позволяют писать код, который работает с разными типами, сохраняя строгую типизацию.

    Вы уже видели это в коллекциях: List<T>, Dictionary<TKey, TValue>.

    Обобщённый класс

    Плюсы:

  • меньше дублирования кода
  • меньше ошибок приведения типов
  • часто лучше производительность, чем использование object (меньше упаковки значимых типов)
  • Ограничения параметров типа

    Иногда нужно потребовать от T конкретные свойства. Для этого есть where.

    where T : new() означает: тип T обязан иметь публичный конструктор без параметров.

    Ещё примеры полезных ограничений:

  • where T : class — только ссылочные типы
  • where T : struct — только значимые типы
  • where T : notnull — запрещает null
  • where T : IDisposable — гарантирует наличие Dispose()
  • Вариантность: out и in на уровне интерфейсов

    Некоторые интерфейсы в .NET поддерживают вариантность, чтобы легче было подставлять «более конкретные» типы туда, где ожидаются «более общие».

    Типичный пример: IEnumerable<out T> ковариантен. Это позволяет использовать IEnumerable<string> там, где ожидается IEnumerable<object>.

    Документация: Вариантность в обобщениях

    LINQ: выразительная работа с коллекциями

    В прошлой статье вы уже видели Where. Теперь закрепим основу так, чтобы вы понимали, что именно происходит.

    LINQ — это набор методов и синтаксических конструкций, которые позволяют:

  • фильтровать (Where)
  • преобразовывать (Select)
  • сортировать (OrderBy)
  • группировать (GroupBy)
  • агрегировать (Count, Sum, Min, Max)
  • проверять условия (Any, All)
  • Метод-синтаксис и query-синтаксис

    Обычно используется метод-синтаксис:

    Query-синтаксис часто читается «как запрос»:

    Фактически query-синтаксис компилятор превращает в цепочку вызовов методов LINQ.

    Ленивая (отложенная) и немедленная (материализующая) работа

    Многие LINQ-операторы возвращают IEnumerable<T> и выполняются лениво: пока вы не начнёте перечисление, вычисления не произойдут.

    Материализующие операторы выполняют вычисление сразу и сохраняют результат:

  • ToList() возвращает List<T>
  • ToArray() возвращает массив
  • Count(), First(), Single() возвращают значение
  • Практическое правило:

  • держите результат как IEnumerable<T>, если хотите построить конвейер преобразований
  • вызывайте ToList() или ToArray(), когда нужно:
  • - зафиксировать результат на текущий момент - избежать повторного перечисления - передать данные дальше как готовую коллекцию

    Частые ошибки с LINQ

  • Повторное перечисление одного и того же IEnumerable<T> может повторно выполнить тяжёлую работу.
  • Побочные эффекты внутри Select и Where ухудшают читаемость и дебаг.
  • First() бросит исключение, если элементов нет. Если отсутствие элемента — норма, используйте FirstOrDefault() и аккуратно обрабатывайте результат.
  • IEnumerable<T> и IQueryable<T>

  • IEnumerable<T> — вычисления обычно происходят в памяти (перебор в .NET).
  • IQueryable<T> часто используется в ORM (например, Entity Framework): выражение запроса может быть преобразовано в SQL.
  • Практический вывод: не все методы и конструкции одинаково переводимы в SQL, поэтому запросы к БД требуют дисциплины и понимания, где именно выполняется LINQ.

    Итоги

    После этой статьи у вас должно получиться связать основы коллекций и типов с «архитектурным» уровнем:

  • классы и свойства позволяют моделировать сущности и защищать инварианты
  • record удобен для моделей данных и даёт равенство по значениям
  • наследование и полиморфизм помогают расширять систему без переписывания существующего кода, но ими важно не злоупотреблять
  • интерфейсы отделяют контракт от реализации и упрощают тестирование
  • generics дают переиспользование и типобезопасность
  • LINQ — основной инструмент выразительной работы с коллекциями, важно понимать ленивость и материализацию
  • Дальше эти знания станут основой для тем про обработку ошибок, асинхронность, тестирование, архитектуру и производительность.

    3. Асинхронность и многопоточность: async/await, TPL, синхронизация

    Асинхронность и многопоточность: async/await, TPL, синхронизация

    Зачем это нужно после ООП, generics и LINQ

    В прошлых темах вы научились моделировать предметную область через классы и интерфейсы, переиспользовать код через generics и выразительно обрабатывать данные через LINQ. Следующий шаг к уровню Middle и выше — уверенно работать с нагрузкой и ожиданием:

  • ожидание I/O (сеть, диск, база данных) без блокировки потока
  • параллельное выполнение CPU-задач
  • безопасный доступ к общим данным при одновременной работе нескольких потоков
  • В .NET это достигается тремя связанными инструментальными областями:

  • асинхронность (async/await, Task) — про не блокировать поток во время ожидания
  • параллелизм (TPL, Parallel, PLINQ) — про использовать несколько ядер CPU
  • синхронизация (lock, SemaphoreSlim, concurrent-коллекции) — про защитить общее состояние
  • Полезные источники:

  • Asynchronous programming with async and await
  • Task-based asynchronous pattern (TAP)
  • Task Parallel Library (TPL)
  • Cancellation in managed threads
  • lock statement
  • Thread-safe collections
  • Базовые понятия: конкуррентность, параллелизм, многопоточность

    Частая проблема на уровне Junior: воспринимать async как «запустить на другом потоке». Это не так.

  • Конкуррентность — несколько задач продвигаются вперёд с чередованием, даже на одном потоке.
  • Параллелизм — несколько задач выполняются одновременно на разных ядрах CPU.
  • Многопоточность — наличие нескольких потоков исполнения; это может быть и про параллелизм, и просто про распределение работы.
  • !Диаграмма различий между конкуррентностью (async) и параллелизмом (TPL/Parallel)

    Task и модель TAP: основа async/await

    В современном .NET стандарт асинхронного API — TAP (Task-based Asynchronous Pattern): методы возвращают Task или Task<T>.

  • Task означает «операция завершится в будущем» без результата.
  • Task<T> означает «операция завершится в будущем и вернёт T».
  • Как работает async/await на практике

    Ключевое поведение await:

  • если задача ещё не завершена, управление возвращается вызывающему коду, а текущий метод приостанавливается
  • поток при этом не обязан быть занят ожиданием
  • после завершения задачи выполнение метода продолжится с места await
  • Пример типичного I/O сценария:

    Что важно:

  • метод не блокирует поток на ожидании сети
  • CancellationToken прокидывается в I/O методы, чтобы запрос можно было отменить
  • Правило async all the way

    Если вы начали асинхронную цепочку, старайтесь делать её асинхронной до самого верха.

    Плохо:

    Хорошо:

    Причина: .Result и .Wait() блокируют поток и могут привести к взаимной блокировке (deadlock) в средах с SynchronizationContext (например, UI-приложения).

    SynchronizationContext и типичный deadlock

    В некоторых приложениях есть контекст синхронизации, который требует продолжать выполнение на том же контексте:

  • UI (WPF/WinForms): продолжение часто должно попасть обратно в UI-поток
  • старые ASP.NET: продолжение может пытаться вернуться в контекст запроса
  • Если вы делаете .Result в UI-потоке, вы блокируете его, а await внутри пытается продолжить на том же UI-потоке, который уже заблокирован.

    В библиотечном коде иногда применяют ConfigureAwait(false), чтобы не пытаться возвращаться в исходный контекст:

    Ссылка: Task.ConfigureAwait

    Практическое правило:

  • в прикладном коде (UI, API) чаще достаточно просто не блокировать и использовать await
  • в библиотеке, которую будут вызывать из разных сред, ConfigureAwait(false) может быть полезен, но требует дисциплины (после него нельзя трогать UI)
  • I/O-bound и CPU-bound: что запускать через await, а что через TPL

    I/O-bound задачи

    Примеры:

  • HTTP-запрос
  • чтение файла
  • запрос к БД
  • Обычно это ожидание, а не вычисления. Правильный подход — использовать асинхронные API и await.

    CPU-bound задачи

    Примеры:

  • сериализация большого объекта
  • вычисления, парсинг, компрессия
  • обработка больших массивов
  • Тут await сам по себе не поможет, потому что CPU занят. Подходы:

  • вынести вычисление в пул потоков через Task.Run
  • распараллелить вычисления через TPL (Parallel, PLINQ)
  • Task.Run уместен, когда вам нужно не блокировать текущий поток (например, UI-поток), а вычисление может выполняться в пуле потоков:

    Важно:

  • не используйте Task.Run для I/O (I/O и так имеет асинхронные API)
  • частый Task.Run под нагрузкой может создавать конкуренцию за потоки пула и ухудшать задержки
  • TPL: параллелизм для CPU-задач

    TPL (Task Parallel Library) — инфраструктура для параллельного выполнения работы, чаще всего на пуле потоков.

    Task.WhenAll и Task.WhenAny

  • Task.WhenAll удобно, когда нужно дождаться всех операций
  • Task.WhenAny — когда важна первая завершившаяся
  • Пример параллельного ожидания нескольких I/O операций:

    Практический смысл: вы не ждёте URL последовательно, а запускаете запросы и ждёте их завершения вместе.

    Parallel.ForEach и PLINQ

    Когда у вас много независимых CPU-операций, можно распараллелить обработку:

    Ограничения и риски:

  • параллелизм не всегда ускоряет: есть накладные расходы на планирование и синхронизацию
  • если внутри есть общий ресурс (например, общий список, общий файл, общий счётчик), нужна синхронизация
  • Ссылка: Parallel class

    Отмена операций: CancellationToken

    Отмена в .NET — кооперативная: вы просите задачу отмениться, а она сама должна это поддержать.

    Базовый паттерн:

  • принимаем CancellationToken в публичных async методах
  • прокидываем токен в системные async API
  • для CPU-циклов периодически проверяем ct.ThrowIfCancellationRequested()
  • Пример:

    Ссылка: Cancellation in managed threads

    Исключения в асинхронном коде

    Важное правило: await разворачивает исключения.

  • если Task завершилась ошибкой, await бросит исключение
  • в случае Task.WhenAll возможны несколько ошибок, они агрегируются
  • Пример обработки:

    Практический вывод: ошибки в асинхронном коде обрабатываются привычным try/catch, но при групповых ожиданиях важно помнить, что задач могло упасть несколько.

    Синхронизация: что ломается в многопоточности

    Если два потока одновременно читают и пишут общие данные без правил, возникают:

  • гонки данных (data races): результат зависит от порядка выполнения
  • нарушение инвариантов: объект оказывается в невозможном состоянии
  • потеря обновлений: два инкремента дают +1 вместо +2
  • !Пример гонки данных при инкременте общего счётчика без синхронизации

    lock и критические секции

    lock защищает участок кода так, чтобы в один момент времени его выполнял только один поток.

    Ссылка: lock statement

    Правила безопасного lock:

  • блокируйте на приватном объекте (private readonly object _gate), а не на this и не на строке
  • держите критическую секцию короткой
  • не делайте внутри lock долгие I/O операции
  • Почему нельзя делать await внутри lock

    await может приостановить метод. Если в этот момент удерживается lock, вы рискуете надолго заблокировать другие потоки. Кроме того, компилятор не позволит написать await внутри lock напрямую.

    Для асинхронной синхронизации используют другие примитивы.

    Атомарные операции: Interlocked

    Если нужно сделать очень простую операцию над числом (например, инкремент), lock может быть избыточен.

    Когда это уместно:

  • простой счётчик, статистика
  • отсутствие сложных инвариантов
  • Когда не уместно:

  • нужно обновлять несколько полей согласованно
  • нужна сложная логика проверки и обновления состояния
  • Потокобезопасные коллекции

    Если несколько потоков читают и пишут в общие коллекции, обычные List<T> и Dictionary<TKey, TValue> не подходят без внешней синхронизации.

    Используйте concurrent-коллекции:

  • ConcurrentDictionary<TKey, TValue>
  • ConcurrentQueue<T>
  • ConcurrentBag<T>
  • Ссылка: Thread-safe collections

    Важно: потокобезопасная коллекция решает проблему конкурентного доступа к структуре, но не отменяет необходимость думать об инвариантах на уровне всей операции.

    Асинхронная синхронизация: SemaphoreSlim

    Если вам нужно ограничить число одновременных операций (например, не больше 5 запросов к внешнему сервису), удобен SemaphoreSlim.

    Ссылка: SemaphoreSlim

    Это одна из ключевых техник в высоконагруженных системах: вы защищаете внешний ресурс (API, БД, файл) от перегрузки.

    Практические правила для production-кода

  • Используйте async Task, а не async void (исключение: обработчики событий).
  • Не смешивайте блокирующие ожидания (.Result, .Wait()) с await.
  • Для I/O используйте асинхронные API, а не Task.Run.
  • Для CPU-задач рассматривайте TPL (Task.Run, Parallel, PLINQ), но измеряйте и контролируйте общие ресурсы.
  • Если есть общий mutable state, определите стратегию: неизменяемость, lock, Interlocked, concurrent-коллекции, семафоры.
  • Итоги

    В этой теме вы связали высокоуровневую модель кода из ООП и коллекций с тем, как приложение ведёт себя под нагрузкой:

  • async/await и Task дают неблокирующее ожидание I/O
  • TPL помогает распараллеливать CPU-нагрузку
  • CancellationToken задаёт стандартный способ отмены
  • синхронизация (lock, Interlocked, concurrent-коллекции, SemaphoreSlim) защищает общее состояние и ресурсы
  • Дальше эти знания обычно применяются в серверной разработке (ASP.NET), в работе с БД, в фоновых обработчиках, а также при оптимизации производительности и устойчивости.

    4. Работа с данными: EF Core, SQL, транзакции, миграции

    Работа с данными: EF Core, SQL, транзакции, миграции

    Зачем эта тема нужна на пути от Junior к Senior

    После тем про ООП, LINQ и асинхронность вы уже умеете:

  • моделировать предметную область через классы и интерфейсы
  • выразительно обрабатывать коллекции через LINQ
  • писать неблокирующий код через async/await
  • Но в реальных приложениях почти всегда есть состояние, которое живёт дольше процесса: пользователи, заказы, платежи, события. Обычно это означает базу данных и слой доступа к данным.

    В этой статье разберём:

  • как EF Core связывает вашу объектную модель (классы) и реляционную модель (таблицы)
  • как писать запросы через LINQ и понимать, что уйдёт в SQL
  • что такое отслеживание изменений и единица работы
  • как управлять схемой БД через миграции
  • как и когда использовать транзакции
  • типичные ошибки производительности и конкурентного доступа
  • Полезные официальные источники:

  • Документация EF Core
  • DbContext (конфигурация и время жизни)
  • Запросы в EF Core
  • Отслеживание запросов (Tracking)
  • Миграции
  • Транзакции
  • Конкурентность (optimistic concurrency)
  • SQL-запросы (FromSql/ExecuteSql)
  • Контекст: EF Core и SQL — как они соотносятся

    SQL — язык запросов к реляционным базам данных (PostgreSQL, SQL Server, MySQL и др.).

    EF Core — ORM (Object-Relational Mapper), библиотека .NET, которая:

  • позволяет описать сущности как классы C#
  • сопоставляет их таблицам и колонкам
  • переводит LINQ-запросы в SQL (там, где это возможно)
  • отслеживает изменения объектов и формирует INSERT/UPDATE/DELETE
  • Важная мысль для роста: EF Core не отменяет необходимость понимать SQL. Senior-уровень обычно подразумевает, что вы:

  • читаете сгенерированный SQL и находите проблемы
  • понимаете индексы, фильтрацию, сортировку, джоины
  • осознанно управляете транзакциями и конкуренцией
  • !Поток: LINQ в C# превращается в SQL и обратно в объекты

    Быстрый старт: модель, DbContext, подключение

    Сущности и DbContext

    Обычно у вас есть:

  • сущности (entity) — классы предметной области, которые хранятся в БД
  • DbContext — “единица работы”, через которую выполняются запросы и сохранение изменений
  • Пример доменной модели и контекста:

    Что здесь важно:

  • DbSet<T> — “таблица” сущностей для запросов
  • навигационные свойства (User.Orders, Order.User) выражают связи, но не отменяют наличия внешнего ключа (Order.UserId)
  • OnModelCreating фиксирует правила маппинга: ключи, индексы, ограничения
  • Время жизни DbContext

    DbContext не потокобезопасен. Нельзя использовать один экземпляр контекста из нескольких потоков одновременно.

    Типичный подход в серверном приложении:

  • один DbContext на один запрос (scoped lifetime)
  • все операции с БД в пределах запроса используют этот контекст
  • Документация: DbContext (конфигурация и время жизни)

    Запросы: LINQ, перевод в SQL и типовые формы

    IQueryable и отложенное выполнение

    Когда вы пишете:

    обычно получается IQueryable<User>. Это означает:

  • запрос ещё не выполнен
  • EF Core строит дерево выражения
  • SQL будет сгенерирован, когда вы начнёте материализацию
  • Материализация — это, например:

  • ToListAsync()
  • FirstOrDefaultAsync()
  • SingleAsync()
  • CountAsync()
  • Проекция: выбирайте только нужное

    Частая ошибка Junior: “достану сущность целиком, а потом выберу пару полей”. Это может быть дорого.

    Правильнее проецировать через Select:

    Плюсы:

  • меньше данных по сети
  • меньше работы на материализацию
  • проще контролировать форму SQL
  • Include и проблема N+1

    Если вам нужны связанные данные, есть загрузка навигаций.

  • eager loading через Include
  • explicit loading через отдельную загрузку
  • lazy loading существует, но часто приводит к неожиданным запросам и сложнее контролируется
  • Пример Include:

    Проблема N+1 выглядит так:

  • вы получаете список пользователей одним запросом
  • затем в цикле для каждого пользователя подгружаете заказы отдельным запросом
  • итог: запросов вместо 1–2
  • Практика: включайте логирование SQL и проверяйте, сколько запросов реально уходит.

    Tracking и AsNoTracking

    По умолчанию EF Core часто выполняет запросы с отслеживанием: change tracker “помнит” загруженные сущности, чтобы потом понять, что изменилось.

    Это полезно для сценариев “прочитал → изменил → сохранил”, но может быть лишним для чистого чтения.

    Для read-only запросов используйте AsNoTracking():

    Документация: Отслеживание запросов (Tracking)

    Сохранение изменений: единица работы и транзакционность

    SaveChanges и что он делает

    SaveChanges() или SaveChangesAsync():

  • вычисляет изменения по tracked-объектам
  • формирует команды INSERT/UPDATE/DELETE
  • выполняет их в базе данных
  • Важная деталь: в рамках одного вызова SaveChanges EF Core обычно гарантирует атомарность операций, используя транзакцию, если нужно. Но вы не должны полагаться на это как на универсальную стратегию для сложных сценариев.

    Асинхронность, отмена и правильные сигнатуры

    Поскольку доступ к БД — типичный I/O, в серверном коде стандарт:

  • методы репозиториев/сервисов делают async Task и принимают CancellationToken
  • EF Core вызывается через ToListAsync(ct), SaveChangesAsync(ct)
  • Это продолжает принцип async all the way из прошлой темы.

    Транзакции: когда они действительно нужны

    Транзакция обеспечивает свойства:

  • атомарность: либо всё применилось, либо ничего
  • согласованность: данные не останутся в “полу-обновлённом” состоянии
  • изоляция: параллельные операции меньше мешают друг другу
  • долговечность: после коммита данные сохранятся
  • В EF Core транзакции нужны явно, когда у вас:

  • несколько вызовов SaveChangesAsync как часть одного бизнес-действия
  • смешивание EF Core и raw SQL в одной атомарной операции
  • несколько контекстов/подключений (реже, сложнее)
  • Пример явной транзакции:

    Что важно:

  • транзакция должна быть как можно короче
  • внутри транзакции нежелательны долгие внешние I/O операции (HTTP, файловая система)
  • Документация: Транзакции

    !Схема: BeginTransaction, команды, Commit/Rollback

    Миграции: управление схемой базы данных как кодом

    Миграции решают задачу: как воспроизводимо менять схему БД между версиями приложения.

    Почему миграции важны

    Без миграций обычно появляется “ручное” изменение схемы:

  • кто-то поправил таблицу на одном окружении
  • забыл повторить на другом
  • в проде схема не совпала с ожиданиями кода
  • Миграции делают изменения:

  • версионируемыми (лежат в репозитории)
  • применимыми на CI/CD
  • воспроизводимыми на новых окружениях
  • Базовый процесс

    Обычно цикл выглядит так:

  • меняете модель или конфигурацию OnModelCreating
  • создаёте миграцию (EF сравнивает модель с текущим снимком)
  • применяете миграцию к базе
  • Документация: Миграции

    Важные практики миграций

  • проверяйте миграцию как код: читаемость, понятные имена, отсутствие опасных операций
  • аккуратно относитесь к операциям, которые могут быть долгими на больших таблицах (например, перестройка индекса)
  • разделяйте “изменение схемы” и “бэкфилл данных”, если это тяжёлая операция
  • !Процесс: модель -> migration -> база данных

    Конкурентность: что делать с одновременными обновлениями

    Если два запроса почти одновременно изменяют одну и ту же запись, возможны “потерянные обновления”.

    Подходы:

  • пессимистическая блокировка (реже, специфично для БД и сценария)
  • оптимистическая конкуренция: “если запись изменили с момента чтения — не перетирать молча”
  • EF Core поддерживает оптимистическую конкуренцию через concurrency tokens.

    Типичный способ: добавить поле версии (например, rowversion в SQL Server) или другое поле, которое участвует в проверке.

    Документация: Конкурентность (optimistic concurrency)

    Практический вывод:

  • важно не только “поймать исключение”, но и определить бизнес-стратегию: повторить попытку, показать пользователю конфликт, объединить изменения
  • Raw SQL: когда EF Core недостаточно

    EF Core удобен, но бывают случаи, когда:

  • нужен специфичный SQL (оконные функции, сложные CTE)
  • нужно добиться предсказуемого плана выполнения
  • проще и безопаснее выразить запрос напрямую
  • EF Core позволяет:

  • выполнять SQL-запросы и маппить результат на типы
  • выполнять команды UPDATE/DELETE без загрузки сущностей
  • Документация: SQL-запросы (FromSql/ExecuteSql)

    Практика:

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

    Список привычек, которые отличают более зрелый уровень работы с данными:

  • включать логирование SQL и анализировать реальное количество запросов
  • избегать N+1 через Include или корректные запросы
  • выбирать только нужные поля через Select
  • использовать AsNoTracking() для отчётов и read-only сценариев
  • не держать DbContext слишком долго
  • не делать параллельные запросы через один DbContext
  • Отдельная дисциплина: индексы и анализ планов выполнения в самой БД. EF Core помогает, но индексы и статистика — зона ответственности разработчика вместе с DBA.

    Итоги

    Теперь у вас есть связная картина слоя данных в .NET-приложении:

  • EF Core связывает ООП-модель с реляционной БД и переводит LINQ в SQL
  • DbContext — это единица работы, его время жизни важно, он не потокобезопасен
  • запросы нужно строить осознанно: проекция, Include, контроль N+1
  • AsNoTracking помогает в read-only сценариях
  • транзакции нужны, когда бизнес-операция включает несколько шагов, которые должны быть атомарны
  • миграции — способ версионировать схему БД и воспроизводимо менять её между окружениями
  • конкурентность и производительность — обязательные темы для уровня Middle/Senior
  • 5. Тестирование и качество: unit/integration, mocking, CI

    Тестирование и качество: unit/integration, mocking, CI

    Зачем тестирование нужно на пути от Junior к Senior

    В прошлых темах вы научились писать код с хорошей структурой (ООП, интерфейсы, generics), понимать поведение программы под нагрузкой (async/await, TPL, синхронизация) и работать с данными (EF Core, транзакции, миграции). Следующий шаг к уровню Middle/Senior — системно обеспечивать качество.

    Качество в разработке на C# обычно означает:

  • код работает корректно и предсказуемо
  • изменения не ломают старое поведение (регрессии ловятся рано)
  • ошибки быстро локализуются
  • сборка и проверка повторяемы (одинаково на ноутбуке и в CI)
  • Тестирование — один из главных инструментов, но само по себе оно не “магия”. Важно понимать, какие тесты писать, что именно проверять, когда использовать моки, и как встроить это в CI.

    Полезные источники:

  • Тестирование в .NET
  • dotnet test
  • Интеграционные тесты в ASP.NET Core
  • Moq (репозиторий)
  • FluentAssertions
  • GitHub Actions (документация)
  • Code analysis в .NET
  • !Пирамида тестов показывает, что разных тестов должно быть разное количество

    Виды тестов и границы ответственности

    Тесты различаются по тому, какой кусок системы они проверяют и какие зависимости используют.

    | Вид теста | Что проверяет | Зависимости | Скорость | Типичная цель | |---|---|---|---|---| | Unit | Логику одного класса/метода | Заменяются тест-дублями (моки/стабы) | высокая | быстро ловить ошибки в логике | | Integration | Взаимодействие компонентов (например, EF Core + БД) | Реальные компоненты (или максимально близкие) | средняя/низкая | ловить ошибки конфигурации, маппинга, SQL | | End-to-end | Сценарий “как у пользователя” | Реальная система целиком | низкая | проверять критические пользовательские потоки |

    Важно: границы между unit и integration определяются не “файлом теста”, а тем, что вы считаете внешней зависимостью. Например:

  • для доменной логики внешняя зависимость — БД, сеть, файловая система, системное время
  • для слоя данных внешняя зависимость — конкретная СУБД и схема, транзакции, индексы
  • Инструменты экосистемы .NET

    Фреймворки тестирования

    Самые распространённые варианты:

  • xUnit — часто используется в современных проектах и open-source
  • NUnit — зрелый и популярный
  • MSTest — встроенная экосистема Microsoft (часто встречается в enterprise)
  • Выбор фреймворка обычно менее важен, чем дисциплина тестирования, читаемость и устойчивость тестов.

    Библиотеки для выразительных проверок

  • FluentAssertions помогает писать проверки так, чтобы они читались как спецификация
  • Mocking

  • Moq (репозиторий) — один из самых известных фреймворков моков в .NET
  • Unit-тесты: цель, структура, принципы

    Unit-тест отвечает на вопрос: “Правильно ли работает этот небольшой кусок логики при разных входах?”

    Принцип Arrange-Act-Assert

    Одна из самых понятных структур:

  • Arrange: подготовить данные и зависимости
  • Act: выполнить действие
  • Assert: проверить результат
  • Пример (xUnit + FluentAssertions):

    Здесь:

  • sut (system under test) — объект, который тестируем
  • тест не зависит от БД/сети/времени и выполняется быстро
  • Каким должен быть хороший unit-тест

  • детерминированным: одинаковый результат при каждом запуске
  • быстрым: иначе их перестают запускать часто
  • изолированным: не зависит от других тестов и порядка выполнения
  • читабельным: по имени теста и телу понятно, что сломалось и почему
  • Типичные ошибки unit-тестов

  • тест “проверяет реализацию”, а не поведение
  • в тесте слишком много условий и веток (непонятно, что именно важно)
  • тест зависит от системного времени, случайных чисел, локали, окружения
  • тесты используют реальную БД и становятся медленными (это уже integration)
  • Mocking и test doubles: что это и когда нужно

    Test double — общее название для “заменителей” зависимостей в тестах. На практике чаще всего обсуждают:

  • stub — возвращает заранее заданные данные
  • mock — позволяет проверить, что зависимость вызвали правильным образом
  • fake — упрощённая реализация (например, in-memory хранилище)
  • Зачем нужны моки

    Моки помогают:

  • изолировать тестируемую логику от внешних систем
  • сделать тест быстрым и предсказуемым
  • проверить, что были выполнены важные взаимодействия (например, “отправили уведомление”)
  • Пример с интерфейсом и Moq:

    Когда моки вредят

    Моки часто ухудшают тесты, если вы:

  • мокаете “всё подряд” и тест превращается в проверку вызовов вместо проверки смысла
  • мокаете собственные классы со сложной логикой вместо того, чтобы тестировать их напрямую
  • мокаете EF Core запросы или DbContext так, что тест перестаёт быть похожим на реальность
  • Практическое правило:

  • мокайте границы системы (внешние зависимости)
  • старайтесь оставлять бизнес-логику тестируемой без моков, через чистые классы
  • Тестирование асинхронного кода

    Из прошлой темы про async/await следует важное: тесты должны ожидать Task, а не блокировать.

    Рекомендации:

  • используйте async Task в тестах
  • не используйте .Result и .Wait() в тестах без крайней необходимости
  • учитывайте отмену через CancellationToken там, где она часть контракта
  • Пример теста на отмену:

    Интеграционные тесты: база данных, EF Core, транзакции

    Интеграционные тесты отвечают на вопрос: “Правильно ли работают компоненты вместе?” Для темы EF Core это особенно важно, потому что:

  • LINQ должен быть переводим в SQL
  • маппинг сущностей может быть неверным
  • транзакции и конкурентность проявляются только на реальной СУБД
  • Стратегии для тестирования EF Core

    Есть несколько вариантов, и у каждого компромиссы:

  • SQLite in-memory
  • реальная БД (локально или в контейнере)
  • SQLite in-memory может быть полезен как быстрый компромисс, но он не воспроизводит поведение PostgreSQL/SQL Server на 100%.

    Для наиболее реалистичных интеграционных тестов всё чаще используют контейнеры:

  • Testcontainers for .NET поднимает нужную СУБД в Docker на время тестов
  • Ключевые правила интеграционных тестов с БД

  • держите тесты независимыми
  • очищайте состояние между тестами (транзакция с rollback, пересоздание схемы, отдельная БД на тест)
  • делайте данные теста минимальными (ровно то, что нужно для сценария)
  • проверяйте не только “что вернулось”, но и что данные действительно записались корректно
  • Интеграционные тесты HTTP API

    Если вы пишете сервер на ASP.NET Core, типовая практика — поднимать приложение “в памяти” и тестировать реальные HTTP-запросы через WebApplicationFactory.

    Документация: Интеграционные тесты в ASP.NET Core

    Покрытие тестами и метрики качества

    Покрытие тестами (coverage) отвечает на вопрос: “Какая доля кода была выполнена при запуске тестов?” Это полезный сигнал, но не цель сама по себе.

    Важно понимать ограничения:

  • высокий coverage не гарантирует наличие проверок смысла
  • низкий coverage не всегда плох (например, если много инфраструктурного кода)
  • Инструменты:

  • coverlet (репозиторий) — сбор покрытия для .NET
  • Практическая позиция “ближе к Senior”:

  • стремиться к хорошему покрытию бизнес-логики
  • не пытаться “набить проценты” тестированием тривиальных геттеров/сеттеров
  • дополнительно использовать статический анализ и строгие настройки компилятора (nullable, анализаторы)
  • Документация по анализаторам: Code analysis в .NET

    CI: как встроить тестирование в процесс разработки

    CI (Continuous Integration) делает проверки автоматическими: каждый pull request и каждый пуш должны собираться и прогонять тесты.

    Минимальный пайплайн для .NET проекта:

  • restore
  • build
  • test
  • !CI пайплайн превращает тестирование в автоматическую, повторяемую проверку

    Пример GitHub Actions workflow

    Что это даёт:

  • одинаковые команды на локальной машине и в CI
  • быстрая обратная связь: сломал тест — увидел сразу
  • меньше “работает у меня”
  • Ссылки:

  • GitHub Actions (документация)
  • dotnet test
  • Практические правила, которые отличают зрелый подход

  • пишите тесты на поведение, а не на внутреннюю реализацию
  • отделяйте доменную логику от инфраструктуры (интерфейсы помогают)
  • используйте моки для границ системы, а не для всего подряд
  • интеграционные тесты на EF Core лучше приближать к реальной СУБД
  • запускайте тесты автоматически в CI и не мерджите “красную” сборку
  • дополняйте тесты статическим анализом и дисциплиной code review
  • Итоги

    Теперь у вас есть связная картина “качества” в .NET:

  • unit-тесты быстро проверяют чистую логику
  • mocking помогает изолировать внешние зависимости, но его легко переиспользовать неправильно
  • интеграционные тесты необходимы для проверки EF Core, транзакций, конфигурации и реального взаимодействия компонентов
  • CI делает проверку автоматической и повторяемой
  • Дальше эти навыки напрямую влияют на архитектуру, устойчивость, скорость разработки и уверенность команды при изменениях.

    6. Производительность и надежность: GC, профилирование, диагностика

    Производительность и надежность: GC, профилирование, диагностика

    Почему эта тема важна на пути от Junior к Senior

    После ООП, async/await, EF Core и тестирования у вас появляется типичная «взрослая» проблема: код работает правильно, но под нагрузкой становится медленным, нестабильным или трудно диагностируемым. На Senior-уровне от разработчика ожидают, что он умеет:

  • находить узкие места по фактам, а не по ощущениям
  • понимать влияние аллокаций и сборщика мусора на задержки
  • отличать CPU-проблемы от I/O-проблем
  • быстро собирать диагностику с тестового/прод окружения и локализовать причину
  • Эта статья связывает предыдущие темы:

  • LINQ и EF Core часто влияют на количество аллокаций и форму запросов
  • async/await влияет на модель выполнения и на то, как измерять задержки
  • тесты и CI помогают удерживать производительность от регрессий
  • Полезные официальные источники:

  • Garbage collection in .NET
  • GC fundamentals
  • dotnet-counters
  • dotnet-trace
  • dotnet-dump
  • dotnet-gcdump
  • DiagnosticSource and Activity
  • BenchmarkDotNet
  • Картина целиком: что обычно «ломает» производительность и надежность

    В прикладной разработке на .NET чаще всего встречаются следующие группы проблем:

  • лишние аллокации, приводящие к частым сборкам мусора
  • случайные «пики» задержек из-за GC или блокировок
  • неоптимальные запросы к базе данных и эффект N+1
  • блокирующие ожидания в асинхронном коде
  • утечки памяти из-за неправильного владения объектами
  • отсутствие наблюдаемости: логов, метрик и трассировок, по которым можно понять причину
  • Ключевое правило диагностики: сначала определить, что именно ограничивает систему.

  • CPU-bound: процессор занят вычислениями
  • I/O-bound: приложение ждёт сеть, базу, диск, внешние API
  • contention-bound: потоки ждут друг друга (блокировки, синхронизация)
  • Память в .NET и сборщик мусора

    Зачем вообще нужен GC

    В .NET память для объектов обычно выделяется в управляемой куче, а освобождение памяти делает GC (Garbage Collector). GC ищет объекты, на которые больше нет ссылок, и освобождает память.

    Практический смысл:

  • вы редко «освобождаете память вручную», но вы влияете на частоту GC количеством и размером аллокаций
  • GC может создавать паузы, которые особенно заметны в сервисах с требованиями к задержкам
  • Поколения GC: почему «много мелких объектов» может быть дорого

    GC использует гипотезу поколений: большинство объектов живёт недолго.

  • Gen 0: новые объекты, самые частые и дешёвые сборки
  • Gen 1: промежуточное поколение
  • Gen 2: долгоживущие объекты, сборки реже, но обычно дороже
  • LOH (Large Object Heap): куча больших объектов, как правило для объектов размером примерно от 85 KB
  • !Визуально показывает, как объекты продвигаются по поколениям и почему сборки Gen2 обычно дороже

    Что важно понимать:

  • частые Gen0 сборки часто нормальны
  • регулярные Gen2 сборки под нагрузкой часто означают проблему с аллокациями или утечками
  • LOH чувствителен к большим временным буферам и массивам
  • Server GC и Workstation GC

    У .NET есть разные режимы GC, ориентированные на разные сценарии:

  • Workstation GC обычно используется в desktop-приложениях
  • Server GC обычно используется в серверных приложениях и может эффективнее работать на многоядерных машинах
  • Выбор режима и его настройка зависят от хостинга и конфигурации, но практический вывод один: если вы видите высокое время в GC, нужно не «крутить флажки», а сначала понять, почему столько аллокаций.

    Документация: GC fundamentals

    Где берутся лишние аллокации в типичном C# коде

    Ниже не «запреты», а зоны, где стоит включать внимательность, особенно в горячих путях.

  • частая конкатенация строк через + в циклах
  • неумеренное использование LINQ в критичных местах, особенно с материализацией через ToList()
  • boxing значимых типов при приведении к object или неудачных интерфейсах
  • замыкания и захват переменных в лямбдах
  • создание больших временных массивов и буферов
  • исключения как часть «обычного» потока управления
  • EF Core tracking в read-only сценариях
  • Пример: строковая конкатенация в цикле

    Плохо для большого количества операций:

    Чаще лучше:

    Причина: string неизменяем, и каждое += создаёт новую строку.

    Пример: EF Core и read-only запросы

    Если вы строите отчёт или просто читаете данные, полезно отключить tracking:

    Практический эффект:

  • меньше работы для change tracker
  • меньше удержание объектов в памяти контекстом
  • часто выше throughput при чтении
  • Как правильно подходить к оптимизациям

    Оптимизация без измерений часто превращается в «переписали код, стало непонятнее, а быстрее ли стало — неизвестно».

    Рабочий цикл выглядит так:

  • Зафиксировать сценарий и метрику.
  • Собрать базовые измерения.
  • Найти узкое место профилировщиком.
  • Сделать минимальное изменение.
  • Повторить измерения и сравнить.
  • В качестве метрики обычно выбирают:

  • latency: задержка на запрос или операцию
  • throughput: сколько операций в секунду система выдерживает
  • использование ресурсов: CPU, память, время в GC
  • Микробенчмарки и BenchmarkDotNet

    Если вы сравниваете две реализации алгоритма или структуры данных, используйте микробенчмарки, а не «померил Stopwatch один раз».

    Инструмент: BenchmarkDotNet

    Идея:

  • прогрев JIT
  • много итераций
  • статистика и сравнение
  • Профилирование и диагностические инструменты .NET

    Набор инструментов dotnet diagnostics: что для чего

    | Инструмент | Для чего обычно используют | Что получаете на выходе | |---|---|---| | dotnet-counters | живые метрики процесса | потоковые значения counters | | dotnet-trace | сбор событий выполнения, CPU, исключений, GC | trace-файл для анализа | | dotnet-dump | снимок памяти процесса (managed dump) | dump-файл для анализа объектов | | dotnet-gcdump | профиль кучи по аллокациям и удержанию | gcdump для анализа памяти |

    Все инструменты официально документированы:

  • dotnet-counters
  • dotnet-trace
  • dotnet-dump
  • dotnet-gcdump
  • Быстрая проверка состояния через dotnet-counters

    Сценарий: у вас есть процесс, который «подтормаживает», и вы хотите понять, не упёрлись ли вы в GC или CPU.

  • Найдите PID процесса.
  • Подключитесь к counters.
  • На что смотреть в первую очередь:

  • % Time in GC
  • количество сборок Gen0/Gen1/Gen2
  • размер managed heap
  • загрузка CPU процесса
  • Если % Time in GC высокий и растёт heap, это сильный сигнал про аллокации или удержание объектов.

    Когда нужен dotnet-trace

    Сценарий: CPU «высокий», но непонятно, где именно время тратится, или есть подозрение на частые сборки.

    Вы получите trace-файл, который можно анализировать в инструментах, понимающих формат trace.

    Когда нужен dotnet-dump

    Сценарий: процесс потребляет всё больше памяти, есть подозрение на утечку или «кэш, который не ограничен».

    После этого dump можно открыть анализатором и посмотреть:

  • какие типы объектов занимают больше всего памяти
  • кто удерживает ссылки на эти объекты
  • Когда нужен dotnet-gcdump

    Сценарий: нужно быстро понять, какие объекты доминируют в куче, без полноценного crash dump.

    Типовые причины «утечек памяти» в managed мире

    В .NET утечки обычно означают не «память пропала», а «объекты продолжают быть достижимыми, поэтому GC не может их убрать».

    Частые причины:

  • подписки на события без отписки
  • долгоживущие статические коллекции и кэши без ограничений
  • удержание DbContext дольше, чем нужно
  • очереди задач, где продюсер быстрее консюмера
  • замыкания, удерживающие большие графы объектов
  • Практический приём мышления: если объект не должен жить долго, у него не должно быть ссылки из долгоживущего корня.

    Надёжность через наблюдаемость: логи, метрики, трассировка

    Хорошая диагностика в production обычно строится на трёх «столпах»:

  • логи: что произошло и с какими параметрами
  • метрики: сколько и как часто (ошибки, задержки, нагрузка)
  • трассировка: как именно прошёл запрос через компоненты
  • !Показывает, как совместно используются логи, метрики и трассировки для диагностики инцидентов

    Корреляция запросов через Activity

    В .NET базовый механизм для трассировки и корреляции — System.Diagnostics.Activity.

  • Activity представляет операцию
  • TraceId позволяет связать события в разных сервисах
  • Пример минимальной ручной разметки:

    Концепции распределённой трассировки описаны здесь: Distributed tracing concepts

    Таймауты и отмена как часть надежности

    Из темы про асинхронность вы уже знаете про CancellationToken. Для надежности важно, чтобы внешние вызовы не могли «повесить» поток запросов бесконечным ожиданием.

    Практики:

  • ставить таймауты на HTTP и запросы к БД
  • прокидывать CancellationToken до границ системы
  • не использовать .Result и .Wait() в прикладном коде
  • Практические советы, которые дают максимальный эффект

    Правила, которые почти всегда окупаются

  • Профилируйте перед оптимизацией.
  • Оптимизируйте горячие пути, а не весь код.
  • Уменьшайте аллокации в циклах и на высоких RPS.
  • Для read-only запросов EF Core используйте AsNoTracking().
  • Не делайте исключения частью обычного управления потоком.
  • Добавляйте наблюдаемость заранее: логи, метрики, Activity.
  • Как связать это с тестами и CI

    Тесты из прошлой темы ловят корректность, а для производительности полезны дополнительные практики:

  • нагрузочные тесты на критичные сценарии перед релизом
  • микробенчмарки на небольшие компоненты, где важны наносекунды и аллокации
  • проверки регрессий: «после PR latency не выросла более чем на X%»
  • Итоги

    После этой темы вы должны уверенно понимать и применять:

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

    7. Архитектура уровня Senior: паттерны, DDD, микросервисы, безопасность

    Архитектура уровня Senior: паттерны, DDD, микросервисы, безопасность

    Зачем эта тема нужна после производительности, данных и тестирования

    К этому моменту курса вы уже умеете писать корректный C# код, понимать async/await, работать с EF Core и SQL, покрывать систему тестами и диагностировать проблемы через GC/профилирование/трейсинг. Следующий скачок к Senior — научиться принимать архитектурные решения так, чтобы система:

  • развивалась без постоянных переписываний
  • выдерживала рост команды и сложности
  • оставалась наблюдаемой и безопасной
  • имела понятные границы ответственности
  • Архитектура Senior-уровня — это не набор модных слов, а дисциплина про границы, контракты, компромиссы и управление рисками.

    Полезные источники:

  • Руководство по микросервисной архитектуре .NET
  • DDD и CQRS паттерны в микросервисах .NET
  • Dependency injection в ASP.NET Core
  • Security в ASP.NET Core
  • OWASP Top 10
  • Microsoft identity platform
  • Как думает Senior: качество архитектуры как набор свойств

    Архитектуру удобно оценивать через качества системы. Они часто конфликтуют, поэтому архитектор делает выбор.

    Ключевые качества:

  • Поддерживаемость: изменения локальны и не разрушают систему.
  • Расширяемость: новые сценарии добавляются без переписывания старого.
  • Надёжность: ошибки изолируются, есть ретраи, таймауты, деградация.
  • Производительность: система укладывается в требования по задержкам и ресурсу.
  • Наблюдаемость: по логам/метрикам/трейсам можно понять, что происходит.
  • Безопасность: данные и доступ защищены на уровне дизайна и реализации.
  • Важно: нет архитектуры, которая одновременно максимизирует всё. Например, микросервисы часто повышают автономность команд, но усложняют наблюдаемость и консистентность данных.

    !Качественные атрибуты и их компромиссы

    Паттерны и стили: как структурировать код и зависимости

    Слой, модуль и компонент: термины без путаницы

  • Слой: логическое разделение ответственности (например, API, приложение, домен, инфраструктура).
  • Модуль: часть кода, которая развивается относительно независимо (например, Billing, Orders).
  • Компонент: развертываемая единица или крупный блок (например, сервис, библиотека).
  • Senior-уровень начинается там, где вы перестаёте смешивать всё в одном слое и начинаете управлять зависимостями между частями.

    Clean Architecture и Hexagonal: идея одна

    У разных школ разные названия, но суть общая:

  • бизнес-логика не должна зависеть от UI, базы данных, очередей и фреймворков
  • внешние зависимости подключаются через интерфейсы
  • направление зависимостей идёт внутрь к домену
  • Это продолжает линию курса про интерфейсы, тестирование и DI.

    !Направление зависимостей к домену

    Практическая структура проекта в .NET

    Один из рабочих вариантов для монолита или модульного монолита:

  • MyApp.Domain — доменная модель и правила
  • MyApp.Application — сценарии (use cases), команды/запросы, транзакционные границы
  • MyApp.Infrastructure — EF Core, внешние клиенты, файловая система, брокер сообщений
  • MyApp.Api — ASP.NET Core контроллеры/эндпоинты, авторизация, сериализация
  • MyApp.Tests — unit и integration
  • Важная дисциплина:

  • Domain не ссылается на Infrastructure
  • Application знает только про контракты (интерфейсы), а реализации живут в Infrastructure
  • Dependency Injection как механизм, но не как архитектура

    DI контейнер в ASP.NET Core решает задачу сборки графа зависимостей, но сам по себе не гарантирует хорошую архитектуру.

    Типичные правила зрелого DI:

  • регистрировать зависимости по интерфейсам, если есть вариативность реализации
  • не вводить интерфейсы там, где нет причины для замены
  • держать жизненные циклы (scoped/singleton/transient) осознанными
  • Документация: Dependency injection в ASP.NET Core

    DDD: как моделировать сложный бизнес без «анемичной модели»

    DDD (Domain-Driven Design) — подход к проектированию, где вы строите код вокруг предметной области и языка бизнеса.

    DDD не обязателен всегда. Он окупается, когда:

  • домен сложный и постоянно меняется
  • много правил, исключений и вариантов поведения
  • важно, чтобы модель была понятна не только разработчикам
  • Ubiquitous Language: общий язык как инструмент качества

    Ubiquitous Language — единый язык, на котором одинаково говорят бизнес и разработчики.

    Признаки, что язык работает:

  • названия классов и методов совпадают с терминами бизнеса
  • правила выражаются в коде прямо, а не в комментариях
  • тесты читаются как спецификация
  • Bounded Context: границы смыслов

    Bounded Context — граница, внутри которой термины и правила имеют однозначный смысл.

    Пример: слово "Заказ" в интернет-магазине и в логистике может означать разные вещи.

    Практический вывод:

  • один большой "универсальный" Order на весь бизнес часто превращается в конфликтную сущность
  • лучше иметь разные модели в разных контекстах и связать их интеграцией
  • !Bounded Context и связи между контекстами

    Entity и Value Object: что есть что

  • Entity (сущность): имеет идентичность, важна «та же самая» сущность во времени (OrderId, UserId).
  • Value Object (объект-значение): идентичности нет, важно значение целиком (например, Money, Email). Обычно делают неизменяемым.
  • Пример value object, который защищает инвариант (валидный email):

    Связь с предыдущими темами:

  • вы используете неизменяемость (как в record), чтобы снижать количество ошибок
  • вы делаете проверку на границах, чтобы не разносить null и невалидные значения по всей системе
  • Aggregate: транзакционная граница домена

    Aggregate (агрегат) — группа объектов, которая должна быть консистентна в рамках одной операции изменения.

  • у агрегата есть Aggregate Root — главный объект, через который разрешены изменения
  • внутри агрегата вы поддерживаете инварианты
  • Пример: Order как aggregate root, который не позволяет добавить отрицательное количество.

    Практический смысл для EF Core и транзакций:

  • агрегат часто соответствует тому, что вы меняете атомарно в SaveChangesAsync
  • это помогает не раздувать транзакции и не пытаться "одной операцией" менять полсистемы
  • Domain Service и Application Service: где какая логика

  • Domain Service: бизнес-правило, которое не принадлежит одной сущности естественным образом.
  • Application Service (use case): оркестрация шагов сценария, работа с репозиторием, транзакцией, интеграциями.
  • Типичный зрелый вариант:

  • домен содержит правила
  • application слой собирает сценарий и управляет зависимостями
  • инфраструктура реализует I/O
  • Репозитории и EF Core: полезное напряжение

    Repository в DDD — абстракция коллекции агрегатов.

    В .NET с EF Core часто выбирают один из подходов:

  • DbContext как реализация Unit of Work + ограниченный слой репозиториев
  • явные репозитории только там, где это упрощает домен и тестирование
  • Ключевые правила из темы EF Core сохраняются:

  • DbContext scoped, не использовать из нескольких потоков
  • проекции через Select, контроль N+1, AsNoTracking для чтения
  • Документация про DDD в микросервисах .NET: DDD и CQRS паттерны в микросервисах .NET

    Микросервисы: когда они нужны и какие проблемы создают

    Микросервис — это не технология, а организационно-архитектурная единица

    Обычно микросервис подразумевает:

  • отдельное развертывание
  • свою модель данных и часто свою базу данных
  • автономность команды
  • Почти всегда цена микросервисов:

  • сложнее эксплуатация и CI/CD
  • сложнее наблюдаемость
  • сложнее безопасность (больше точек входа)
  • сложнее консистентность данных (нет простых транзакций на всё)
  • Когда лучше не делать микросервисы

    Признаки, что микросервисы сейчас навредят:

  • маленькая команда и нет потребности в независимых релизах
  • домен ещё не стабилен, границы контекстов не ясны
  • нет зрелых практик мониторинга, логирования и инцидент-менеджмента
  • Часто лучший шаг между "монолитом" и "микросервисами" — модульный монолит:

  • один деплой
  • строгие границы модулей
  • явные контракты между модулями
  • Согласованность данных: почему распределённая транзакция почти всегда плохая идея

    В монолите вы легко делаете транзакцию на несколько таблиц.

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

    Чтобы это работало, нужны паттерны надёжной интеграции.

    Outbox: надёжная публикация событий

    Проблема:

  • вы сохранили изменения в БД
  • вам нужно отправить событие в брокер сообщений
  • если приложение упало между этими шагами, система рассинхронизируется
  • Outbox решает это так:

  • вместе с бизнес-данными в той же транзакции записывается запись в таблицу outbox
  • отдельный фоновый процесс читает outbox и публикует события
  • Это связывает тему EF Core, транзакций и надёжности.

    !Outbox для атомарности БД и публикации событий

    Идемпотентность: защита от повторной обработки

    В распределённых системах повторы нормальны:

  • ретраи
  • повторная доставка сообщения
  • таймауты
  • Идемпотентная операция даёт тот же итог при повторном выполнении.

    Практика:

  • использовать ключ идемпотентности (например, RequestId)
  • хранить обработанные ключи и не выполнять действие дважды
  • Устойчивость: таймауты, ретраи, лимиты параллелизма

    Это продолжает тему async/await и SemaphoreSlim:

  • всегда задавайте таймауты для внешних вызовов
  • используйте ретраи только для безопасных операций и с backoff
  • ограничивайте параллелизм к внешним ресурсам
  • В .NET для клиентской устойчивости часто используют Polly, но важно сначала понять принципы.

    Наблюдаемость микросервисов

    То, что в монолите было "просто логом", в микросервисах требует корреляции.

    База:

  • сквозной TraceId через Activity
  • структурированные логи
  • метрики по RPS, latency, error rate
  • Это напрямую связывается с темой диагностики и Activity из прошлой статьи.

    Безопасность как часть архитектуры, а не «после релиза»

    Модель угроз: что защищаем и от кого

    Senior-уровень начинается с вопроса: какие активы у нас есть и какие риски реальны.

    Активы:

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

    OWASP Top 10 как чеклист типовых уязвимостей

    OWASP Top 10 — полезная карта рисков веб-приложений.

    Источник: OWASP Top 10

    С архитектурной точки зрения особенно важны:

  • контроль доступа
  • аутентификация и управление сессиями
  • инъекции
  • утечки данных
  • SSRF
  • Аутентификация и авторизация: разделяйте ответственность

  • Аутентификация отвечает на вопрос "кто ты?"
  • Авторизация отвечает на вопрос "что тебе можно?"
  • В ASP.NET Core это поддерживается стандартными middleware.

    Документация:

  • Security в ASP.NET Core
  • Microsoft identity platform
  • Практики зрелого уровня:

  • применять принцип наименьших привилегий: доступ только к необходимому
  • хранить роли и права как часть дизайна, а не как if-ы по всему коду
  • выносить правила в политики авторизации
  • Валидация входных данных и защита от инъекций

    Ключевая мысль: доверять нельзя ни запросу пользователя, ни данным из другого сервиса.

    Практики:

  • валидировать DTO на границе API
  • в EF Core не собирать SQL строками, использовать параметризацию
  • ограничивать размеры входных данных и сложность запросов
  • Секреты и конфигурация

    Нельзя хранить секреты в репозитории. В архитектуре должны быть предусмотрены:

  • секрет-хранилище (например, переменные окружения или специализированные сервисы)
  • ротация ключей
  • разделение окружений
  • Даже если вы не внедряете конкретный продукт, важно заложить механизм, а не "потом добавим".

    Логи и персональные данные

    Наблюдаемость легко превращается в утечку.

    Правила:

  • не логировать пароли, токены, полные номера карт
  • маскировать PII, если она появляется в логах
  • управлять сроками хранения логов
  • Supply chain: зависимости как часть атаки

    В .NET вы зависите от NuGet пакетов.

    Практики:

  • фиксировать версии
  • обновлять зависимости регулярно
  • включать проверки уязвимостей в CI
  • Как связать всё вместе в одном сценарии

    Архитектура Senior-уровня обычно строится от границ:

  • API слой принимает запрос, аутентифицирует, авторизует, валидирует
  • application слой выполняет сценарий, управляет транзакцией, вызывает домен
  • домен обеспечивает инварианты и понятную модель
  • инфраструктура делает I/O (EF Core, внешние сервисы)
  • события интеграции публикуются надёжно (например, через outbox)
  • тесты проверяют доменную логику и интеграцию, CI не даёт сломать качество
  • наблюдаемость позволяет доказательно диагностировать проблемы
  • Итоги

    После этой темы у вас должна появиться “карта” Senior-подхода:

  • архитектура измеряется качественными атрибутами и компромиссами
  • Clean Architecture помогает управлять зависимостями и тестируемостью
  • DDD даёт инструменты моделирования сложного бизнеса через bounded contexts, value objects и агрегаты
  • микросервисы полезны при определённых организационных и технических условиях, но требуют паттернов надёжности и наблюдаемости
  • безопасность должна быть встроена в дизайн: аутентификация, авторизация, валидация, секреты, контроль утечек
  • Эта тема соединяет весь курс в практическую систему взглядов: от синтаксиса и коллекций до устойчивой, наблюдаемой и безопасной архитектуры.