惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
Scott Helme
Scott Helme
W
WeLiveSecurity
N
News | PayPal Newsroom
G
GRAHAM CLULEY
SecWiki News
SecWiki News
V2EX - 技术
V2EX - 技术
Security Latest
Security Latest
H
Heimdal Security Blog
L
LINUX DO - 最新话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
Spread Privacy
Spread Privacy
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
罗磊的独立博客
C
CXSECURITY Database RSS Feed - CXSecurity.com
宝玉的分享
宝玉的分享
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
Martin Fowler
Martin Fowler
The Hacker News
The Hacker News
C
Cisco Blogs
T
Tor Project blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The Register - Security
The Register - Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Privacy & Cybersecurity Law Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Security Affairs
T
Tenable Blog
V
Visual Studio Blog
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
美团技术团队
月光博客
月光博客
J
Java Code Geeks
量子位
Vercel News
Vercel News
I
Intezer
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
aimingoo的专栏
aimingoo的专栏

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Паттерны и подходы разработки, о которых все говорят — на примере .NET
barsgroup_blog · 2026-06-19 · via Все публикации подряд на Хабре

Средний

19 мин

0

Эта статья о паттернах объектно‑ориентированного проектирования и архитектурных паттернах на платформе .NET. Предполагается, что вы уже знакомы с C#, писали разные виды приложений на .NET и понимаете разницу между наследованием интерфейса и наследованием реализации. При этом материал будет понятен и разработчикам с опытом на других платформах: многие решения, о которых пойдет речь, имеют аналоги вне .NET, а здесь мы разберем, как они реализованы в платформе .NET.

Мы посмотрим на архитектуру .NET не как на набор библиотек, а как на набор реализованных паттернов — от Composition Root и Dependency Injection до Options и HTTP‑конвейера. Пройдем путь от обычной библиотеки классов до Web API, используя уже встроенные в платформу механизмы.

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

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

Поговорим про паттерны проектирования и решения типичных задач. Обсудим базовые паттерны, такие как Dependency Injection, цепочки middleware, паттерны конфигурации, работу с жизненным циклом объектов, организацию хоста и многое другое.

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

В результате код станет более понятным, будем понимать с чем работаем, уменьшим связанность кода, повысим доступность тестирования, снизим количество ошибок и повысим предсказуемость поведения приложения. Знание встроенных инструментов на основе паттернов позволяет эффективно использовать саму платформу .NET, не изобретая велосипед там, где уже есть продуманная архитектура, созданная инженерами Microsoft. Пройдем путь создания Web API из обычной библиотеки классов с использованием готовых паттернов в .NET.

Создаем из библиотеки консольное приложение

Начну свой рассказ с обычной библиотеки классов. Я создал у себя библиотеку классов, назвал ее MyApp.

Библиотека — это набор абстракций, контрактов и их реализаций, но не приложение. У неё нет точки входа, нет жизненного цикла, нет контекста выполнения. Она не знает, кто и как будет использовать её код. Это правильно с точки зрения библиотеки, но мы тут не за этим, нам нужно будет сделать полноценный Web API.

Для начала из нашей библиотеки сделаем консольное приложение. Согласно документации .NET, проект становится исполняемым при наличии точки входа (метод Main или Top-level statements) и настройке типа вывода OutputType=Exe в файле проекта. Современные SDK-style проекты подразумевают эти параметры, при этом классический метод Main обязателен, если сборка не указана как библиотека (DLL). Подробную информацию можно найти в официальной документации Microsoft.

С помощью любого текстового редактора или IDE откройте ваш .csproj файл. Добавьте Exe в
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

Создайте новый класс:
internal class Program

{
   public static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}

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

Поговорим, что произошло на самом деле. На первый взгляд может показаться, что Program.cs — это просто техническая необходимость. На самом деле это ключевая архитектурная точка всего приложения. Именно здесь мы впервые сталкиваемся с понятием Composition Root. Это архитектурный паттерн, который определяет единственное место, где создаются объекты, связываются зависимости, выбираются конкретные реализации, происходит инициализация приложения. В нашем случае эту роль выполняет файл Program.cs. Ключевая идея Composition Root очень проста: вся композиция приложения должна происходить в одном месте. Пример не самый простой. Библиотека не должна создавать свои зависимости, бизнес-код в отдельной библиотеке не должен знать, как именно инициализируется приложение. Решение о том, что с чем связано, а то есть предоставление реализаций и контрактов, принимается снаружи — в нашем Program.cs, в Composition Root.

Но стоит подчеркнуть, что Composition Root — это архитектурный паттерн, а не объектно-ориентированный. В типовом .NET-приложении точка композиции (Composition Root) часто находится в Program.cs, но это не требование платформы. Главное — чтобы все зависимости регистрировались и создавались в одном месте.

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

Добавляем отдельную библиотеку для логирования

Добавим в наше решение еще один проект — библиотеку классов. Назовите ее, к примеру, MyLogger. В созданном проекте MyLogger создайте интерфейс IMyLogger c модификатором доступа public и опишите метод SendLog, который принимает тип String и возвращаемый тип void. Cоздайте класс ConsoleLogger с модификатором доступа public и наследуйте его от IMyLoggerи, реализуйте метод SendLog, который вызывает Console.WriteLine (“переданный параметр типа Srting”). Теперь добавите в MyApp ссылку на проект MyLogger. В методе Main создайте экземпляр ConsoleLogger и присвойте его переменной типа IMyLogger, вызовите SendLog и передайте туда текст сообщения. Запустите проект MyApp: если вы увидели сообщение в консоли, то вы все сделали правильно.

Пример кода. В MyApp.csproj добавился элемент где лежит .csproj вашей библиотеки.

<ItemGroup>
    <ProjectReference Include="..\MyLogger\MyLogger.csproj" />
 </ItemGroup>

В IMyLogger.cs

public interface  IMyLogger
{
    void SendLog(string message);
}

В ConsoleLogger.cs

namespace MyLogger;
public class ConsoleLogger : IMyLogger
{
    public void SendLog(string message)
    {
        Console.WriteLine($"Log: {message}");
    }
}

В MyApp, Program.cs

namespace MyApp;
internal class Program
{
    static void Main(string[] args)
    {
        IMyLogger logger = new MyLogger.ConsoleLogger();
        logger.SendLog("Hello, World!");
    }
}

На этом этапе мы еще не используем DI-контейнер, но уже опираемся на несколько принципов объектно-ориентированного проектирования. В частности:

  • Composition Root в .NET приложение часто находится в Program.cs (или в другом месте запуска), но главное — это единственное место, где собирается объектный граф и регистрируются зависимости. Это не привязано к конкретному файлу.

  • Composition over Inheritance: мы не расширяем библиотеку через наследование и не вшиваем в нее логику запуска. Вместо этого приложение компонуется снаружи, используя готовые компоненты. В нашем случае мы реализовали контракт MyLogger.IMyLogger и можем использовать его разные реализации в одном месте.

  • Dependency Inversion (на уровне идеи): библиотека работает с контрактом на реализацию. Решение о том, какие конкретные реализации использовать, выносится в точку композиции. Таким образом, мы можем описывать множество реализаций логирования без изменения бизнес-кода.

На текущем этапе все выглядит достаточно просто и понятно: у нас есть одна реализация логгера, один контракт и одна точка композиции в Program.cs. Но такое состояние очень быстро перестает быть реальным для живого приложения. Как только приложение начинает расти, обычно происходит следующее:

  • увеличивается количество контрактов сервисов и реализация, количество зависимостей;

  • появляются цепочки зависимостей (один сервис зависит от другого) и исходная точка смещается за потоком управления;

  • в Program.cs начинает концентрироваться всё больше кода по созданию объектов, управлению их жизненным циклом.

Управление временем жизни объектов становится неявным и трудно контролируемым. Жизненный цикл приложения фактически определяется тем, что код в Main выполняется до конца: как только весь код будет выполнен, приложение завершится. Чтобы этого избежать, приходится придумывать собственные решения для контроля жизненного цикла.

Ручное создание объектов через new начинает приводить к ошибкам и дублированию кода. В этот момент Program.cs, который изначально был аккуратным Composition Root, начинает превращаться в место, где сложно понять:

  • какие зависимости у каких компонентов;

  • кто за что отвечает;

  • кто должен быть (Singleton) — тяжелые объекты без состояния или те, что управляют общими ресурсами вроде кэша и соединений;

  • какие компоненты должны создаваться заново для каждого использования (Transient) — легкие сервисы, которые хранят временные данные или небезопасны для потоков;

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

Может показаться, что проблема в расположении Composition Root, но на самом деле проблема в ручной композиции. Нам нужен инструмент, который возьмет на себя решение всех этих вопросов.

Добавляем жизненный цикл приложения и Composition Root через Generic Host

Установите в проект MyApp пакет Microsoft.Extensions.Hosting через nuGet. Замените код в файле Program.cs на:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyLogger;

namespace MyApp;

internal class Program
{
    static void Main(string[] args)
    {
        var host = Host.CreateDefaultBuilder(args)
        .ConfigureServices(services =>
        {
            services.AddSingleton<IMyLogger, ConsoleLogger>();
            services.AddHostedService<Worker>();
        })
        .Build();

        host.Run();
    }
}

public class Worker : IHostedService
{
    private readonly IMyLogger _logger;
    public Worker(IMyLogger logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.SendLog("App starting.");
        return Task.CompletedTask;
    }
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.SendLog("App stopping.");
        return Task.CompletedTask;
    }
}

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

Log: App starting. info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production info: Microsoft.Hosting.Lifetime[0] Content root path: ...\bin\Debug\net9.0 info: Microsoft.Hosting.Lifetime[0] Application is shutting down…

Нажав комбинацию Ctrl+C (Stop button) на Windows или Command+C на macOS, вы увидите в консоли следующий лог:

Log: App stopping.

После этого приложение завершит работу, например:

…\bin\Debug\net9.0\MyApp.exe (процесс 24368) завершил работу с кодом 0 (0x0).

Нажмите любую клавишу, чтобы закрыть это окно.

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

Пакет Microsoft.Extensions.Hosting предоставляет реализацию Generic Host — механизма запуска и управления приложением. Именно Host теперь отвечает за то, когда приложение стартует, сколько оно живет и как корректно завершается.

Начнём с:

Host.CreateDefaultBuilder(args) .ConfigureServices(...) .Build();

HostBuilder использует fluent API в стиле builder для конфигурации Generic Host. В терминах паттернов это можно интерпретировать как Builder, но официально это описано как API конфигурации хоста. Он отвечает не за создание одного объекта, а за построение всего приложения как единого целого. Архитектурно Builder решает несколько задач: отделяет конфигурацию от создания, позволяет пошагово описывать, каким будет приложение, и скрывает сложность инициализации инфраструктуры. HostBuilder накапливает конфигурацию, а метод Build является единственной точкой, где создается финальный объект IHost.

Реализация Composition Root теперь переезжает в:

.ConfigureServices(services =>
{
    services.AddSingleton<IMyLogger, ConsoleLogger>();
    services.AddHostedService<Worker>();
})

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

Это, по сути, и есть теперь Composition Root — место, в котором создаются и связываются зависимости. Не просто Program.cs как файл, а именно часть приложения, где мы настраиваем и регистрируем зависимости через ConfigureServices.

Выбираем конкретную реализацию (ConsoleLogger) для абстракции (IMyLogger), указываем, какие сервисы участвуют в жизненном цикле приложения, и определяем, как эти реализации создаются и как долго они живут.

В ConfigureServices используется встроенный DI-контейнер .NET, который самостоятельно управляет временем жизни сервисов, создает их и передает зависимости в конструкторы зарегистрированных компонентов. Параметр services, который имеет тип IServiceCollection, позволяет нам конфигурировать зависимости контейнера, сопоставлять конкретные реализации контрактам и настраивать их под разные задачи.

Когда мы вызываем Build(), происходит создание конечного DI-контейнера (IServiceProvider) на основе регистраций, собранных в ConfigureServices. Метод Build() не является классическим паттерном Factory, а скорее завершает работу Builder’а (HostBuilder) и строит итоговый объект IHost, включая IServiceProvider.

Таким образом, .NET поддерживает проектирование приложений с использованием внедрения зависимостей: сервисы регистрируются в IServiceCollection, после чего создается IServiceProvider, который выступает контейнером всех зарегистрированных служб. Этот механизм реализует паттерн Dependency Injection и принцип Inversion of Control, когда класс сам не создаёт свои зависимости, а получает их от контейнера.

На этом этапе у нас сформировалась полноценная основа приложения .NET. Точка входа по-прежнему находится в Program.cs, но ее роль принципиально изменилась. Теперь это не место ручного создания объектов, а точка описания конфигурации и композиции приложения. Мы передали управление жизненным циклом, запуском приложения, корректным завершением, созданием и уничтожением объектов, а также контролем времени жизни зависимостей — все это теперь находится в ответственности Generic Host.

Архитектурно у нас выстроилась четкая цепочка:

  • Builder (Host.CreateDefaultBuilder) пошагово описывает, каким будет приложение.

  • Composition Root, место композиции нашего приложения, переезжает в ConfigureServices, где мы связываем контракты и реализации. В .NET DI контейнер создается во время вызова Build(), который строит IHost. После этого IHost.Services предоставляет IServiceProvider, который является контейнером зависимостей.

  • Dependency Injection и Inversion of Control позволяют компонентам получать зависимости, не зная, как и когда они создаются. Именно с этого момента наше приложение перестает быть «консольной программой» в классическом смысле и превращается в приложение с управляемым жизненным циклом и централизованной композицией зависимостей.

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

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

Конфигурация приложения, настройки, IOptions

Теперь давайте добавим настройку нашей реализации Worker. Например, бизнес хочет менять префикс перед сообщением. Какие варианты приходят сразу в голову? Добавить поле в Worker, создать статический класс для хранения глобальных настроек или создать контракт и его реализацию и зарегистрировать ее как зависимость. Все эти подходы нарушают Dependency Inversion и SRP (принцип единственной ответственности). При развитии проекта они приведут к неудобству использования, усложнят поддержку, не позволят конфигурировать приложение извне, ухудшат тестируемость и предсказуемость поведения: любое изменение потребует пересборки проекта.

С точки зрения объектно-ориентированного проектирования конфигурация — это входные данные приложения, часть контекста выполнения, зависимость, а не «магическое знание». Классические подходы, описанные выше, не позволяют выразить это корректно и безопасно.

Если мы постараемся декомпозировать задачу конфигурации Worker, то увидим, что нам нужно:

  • получать данные конфигурации извне;

  • хранить их в удобной форме;

  • спроецировать их в модель данных;

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

В платформе .NET уже есть реализация, которая решает все эти задачи — давайте добавим ее в наш проект.

Добавьте новый класс в проект:

public class WorkerOptions
{
    public bool Enabled { get; set; }
    public string Prefix { get; set; } = string.Empty;
}

Добавьте файл appsettings.json и в нем пропишите:

{
  "WorkerOptions": {
    "Enabled": true,
    "Prefix": "[MyApp]"
  }
}

Зарегистрируйте теперь Options в Composition Root:

.ConfigureServices((context, services) =>
{            
   services.Configure<WorkerOptions>(context.Configuration.GetSection(nameof(WorkerOptions)));

    services.AddSingleton<IMyLogger, ConsoleLogger>();
    services.AddHostedService<Worker>();
})

Обратите внимание: переданная строка в context.Configuration.GetSection(), должна соответствовать названию объекта json, а поля в json — наименованию полей в модели WorkerOptions.

Добавьте в конструктор Worker получение options и добавьте префикс из options к логам:

 public class Worker : IHostedService
{
    private readonly IMyLogger _logger;
    private readonly WorkerOptions _options;
    public Worker(IMyLogger logger, IOptions<WorkerOptions> options)
    {
        _logger = logger;
        _options = options.Value;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.SendLog($"{_options.Prefix}: App starting.");
        return Task.CompletedTask;
    }
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.SendLog($"{_options.Prefix}: App stopping.");
        return Task.CompletedTask;
    }
}

Откройте файл вашего проект (MyApp.csproj) и добавьте туда:

<ItemGroup>
	<None Update="appsettings.json">
		<CopyToOutputDirectory>Always</CopyToOutputDirectory>
	</None>
</ItemGroup>

Это инфраструктурная настройка MSBuild, которая определяет, какие внешние ресурсы должны быть доступны приложению во время выполнения. Запустив приложение, вы увидите, что в сообщениях появился префикс ([MyApp]:). Что за магия тут происходит? За это отвечают Configuration Binding и Options Pattern.

Configuration Binding — это не паттерн GoF. С точки зрения ООП это технический механизм отображения данных на объектную модель. В научной и инженерной литературе это чаще описывают как Data Mapping, Object Binding или External Configuration Mapping. Он решает узкую, но критически важную задачу: преобразовать внешнее представление конфигурации (в нашем случае JSON) в типизированный объект. Также binding работает с другими источниками конфигурации в .NET, например, с переменными окружения или аргументами командной строки. Configuration Binding — это инфраструктурный механизм .NET, который сопоставляет значения конфигурации с объектами.

Документация Microsoft: https://learn.microsoft.com/dotnet/core/extensions/configuration.

Это закрывает сразу несколько задач:

  • бизнес-код работает с типами и у нас появляется строгая типизация;

  • ошибки конфигурации можно обнаружить раньше;

  • настройка и обновление выполняются извне и не требуют пересборки проекта.

Configuration Binding — инфраструктурный механизм, который делает возможным Options Pattern.

Options pattern — это рекомендуемый механизм конфигурации в .NET, реализованный через IOptions<T> и набор расширений Microsoft.Extensions.Options.

Документация Microsoft: https://learn.microsoft.com/dotnet/core/extensions/options.

Главная идея Options Patter — предоставить гибкий, расширяемый и типобезопасный способ конфигурирования объектов, передать конфигурацию как зависимость, а зависимости должны передаваться через конструктор. Options Pattern делает конфигурацию объектом, который:

  • живет с понятным временем жизни;

  • внедряется через DI;

  • инкапсулирует нас от сложной логики.

В .NET для работы с Options Pattern существует набор контрактов в Microsoft.Extensions.Options.

На этом этапе у нас уже есть всё необходимое для построения Web API: управляемый жизненный цикл приложения, централизованная точка композиции, контейнер зависимостей, типобезопасная конфигурация и инфраструктура запуска. С архитектурной точки зрения наше приложение уже является серверным приложением .NET. Единственное, чего ему не хватает — это общения с внешним миром и взаимодействия с нашей системой через HTTP. В .NET это реализуется через Web Host и middleware pipeline.

От консольного приложения к Web API

Теперь кратко о том, с чем мы работаем. В Web API на платформе .NET множество компонентов реализуют паттерны. Рассмотрим примеры.

Компонент — Паттерн:

  • WebApplicationBuilder — Builder

  • Endpoint — Command

  • Controller / Minimal API — Application Service

  • HttpContext — Context Object

  • Filters — Decorator

  • Routing — Strategy

  • Model Binding — Data Mapping

Из нашего приложения (MyApp) мы можем сделать Web API (HTTP), используя перечисленные выше компоненты. До этого мы рассматривали приложение как автономный процесс, выполняющий свою логику внутри управляемого жизненного цикла. Однако большинство современных серверных приложений должны взаимодействовать с внешним миром по сети. С архитектурной точки зрения Web API — это не отдельный тип приложения, а расширение Generic Host, в котором поверх существующей инфраструктуры добавляется HTTP-конвейер обработки запросов.

Эволюция в Web API

Откройте MyApp.csproj и измените:

<Project Sdk="Microsoft.NET.Sdk">

на

<Project Sdk="Microsoft.NET.Sdk.Web">

Замените код в Program.cs на:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace MyApp;

internal class Program
{
    static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Logging.AddConsole();

        builder.Services.Configure<WorkerOptions>(
            builder.Configuration.GetSection(nameof(WorkerOptions)));
        builder.Services.AddHostedService<Worker>();

        builder.Services.AddEndpointsApiExplorer();

        var app = builder.Build();

        app.Use(async (context, next) =>
        {
            var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
            logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");

            await next();
        });

        app.MapGet("/ping", ([FromServices] ILogger<Program> logger) =>
        {
            logger.LogInformation("Ping endpoint called");
            return Results.Ok("pong");
        });

        app.MapGet("/config", ([FromServices] IOptions<WorkerOptions> options) =>
        {
            return Results.Ok(options.Value);
        });

        app.Run();
    }
}

Удалите наш контракт ILogger и его реализацию, и замените код в Worker , так как в Sdk.Web есть реализация Logger и нам свой больше не нужен:

public class Worker : IHostedService
{
    private readonly ILogger<Worker> _logger;
    private readonly WorkerOptions _options;

    public Worker(ILogger<Worker> logger, IOptions<WorkerOptions> options)
    {
        _logger = logger;
        _options = options.Value;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation($"{_options.Prefix}: App starting.");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation($"{_options.Prefix}: App stopping.");
        return Task.CompletedTask;
    }
}

Очистите проект и соберите его заново. После этого в проект должны добавиться некоторые файлы, включая launchSettings.json. Найдите этот файл и измените его содержимое на:

{
  "profiles": {
    "http": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "ping",
      "dotnetRunMessages": true,
      "applicationUrl": "http://localhost:5145"
    }
  }
}

Если кратко: это инструкции для хоста. launchSettings.json автоматически подхватывается MSBuild только при запуске проекта из IDE или командой dotnet run.

Документация Microsoft: https://learn.microsoft.com/visualstudio/ide/run-debug-settings?view=vs-2022#launchsettingsjson.

Отмечу только, что environmentVariables относится к Configuration Binding и Options Pattern — при желании можете углубиться в эту тему отдельно. launchBrowser и launchUrl определяют, нужно ли автоматически открывать браузер при запуске приложения и какую страницу открывать по умолчанию. Остальное  — это уже детали, которые в рамках этой статьи не раскрываем.

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

info: MyApp.Worker[0]
      [MyApp]: App starting.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5145
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: …\MyApp
info: MyApp.Program[0]
      Request: GET /ping
info: MyApp.Program[0]
      Ping endpoint called

В браузере откроется http://localhost:5145/ping и на странице вы увидите ответ pong. По адресу http://localhost:5145/config вы увидите JSON с конфигурацией, а в консоли — логи из middleware и endpoint.

Что изменилось: как проходит HTTP-запрос

Microsoft.NET.Sdk.Web не добавляет «какой-то отдельный Web-движок». Он  подключает набор пакетов и конвенций, включая:

  • ASP.NET Core

  • набор пакетов для работы Kestrel

  • Middleware pipeline

  • WebApplicationBuilder

Но это не просто «встраивает HTTP-сервер», это подключает инфраструктуру для web-приложения.

При вызове WebApplication.CreateBuilder(args) происходит следующее:

  • Generic Host (IHostBuilder) — основной каркас приложения, управляет жизненным циклом и DI.

  • HostBuilderContext — содержит конфигурацию, окружение и прочие параметры хоста.

  • IServiceCollection — коллекция сервисов для регистрации зависимостей (DI).

  • WebApplicationOptions — настройки веб-приложения.

  • KestrelServerOptions — если используется Kestrel, builder автоматически настраивает сервер.

  • Регистрируем WorkerOptions через Options Pattern.

  • Регистрируем Worker как IHostedService (фоновая задача).

  • Все зависимости теперь централизованно управляются DI-контейнером.

Что происходит при builder.Build()

При вызове builder.Build() создаётся WebApplication:

  • IServiceProvider — финальный DI-контейнер, в котором уже все зависимости разрешены.

  • Generic Host (IHost) управляет жизненным циклом приложения. Он вызывает StartAsync и StopAsync у зарегистрированных IHostedService.

  • Middleware pipeline поднимается поверх Generic Host.

  • Kestrel / HTTP server запускается и готов принимать запросы.

Как проходит HTTP-запрос. Когда приходит запрос от клиента, он проходит путь:

Client → Kestrel → HttpContext → Middleware → Endpoint → Response

HttpContext — объект, который инкапсулирует состояние текущего HTTP-запроса и предоставляет доступ к сервисам и ответу. Он не является фасадом Kestrel, а частью ASP.NET Core HTTP pipeline.

Заключение

Мы прошли путь от простой библиотеки классов до полностью управляемого приложения .NET с готовой основой для Web API. Этот процесс наглядно демонстрирует, как паттерны объектно-ориентированного проектирования и архитектурные принципы встроены в платформу .NET и используются ежедневно, часто незаметно.

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

Ключевые выводы:

  • Composition Root — центральная точка композиции приложения, где связываются зависимости и конфигурация. Program.cs перестает быть просто точкой входа и превращается в архитектурный центр приложения.

  • Dependency Injection / IoC — сервисы получают зависимости извне, что снижает связанность, повышает тестируемость и предсказуемость приложения.

  • Builder — WebApplicationBuilder и Host.CreateDefaultBuilder позволяют пошагово собрать приложение как единое целое, скрывая сложность инфраструктуры.

  • Factory — метод Build() создает IHost, а IHost.Services — это уже IServiceProvider коллекция описаний сервисов. DI-контейнер и управляет временем жизни объектов, появляется после вызова BuildServiceProvider() / после Build() хоста.

  • Options Pattern + Configuration Binding — типобезопасная конфигурация через DI; конфигурационные данные проецируются на объекты, соблюдая принципы Dependency Inversion и SRP.

  • Generic Host — управляет жизненным циклом приложения, запускает IHostedService и обеспечивает создание DI-scope’ов для сервисов в рамках хоста. Scope для каждого HTTP-запроса создаёт именно ASP.NET Core (Web Host / WebApplication), когда включен HTTP pipeline.

  • Kestrel + HttpContext — сервер и объект контекста запроса, через который middleware и endpoint получают scoped-зависимости.

  • Middleware Pipeline — Middleware pipeline реализован как конвейер (pipeline) — последовательность делегатов, где каждый middleware может выполнить работу и вызвать следующий. Это концептуально близко к Chain of Responsibility.

    Документация Microsoft: https://learn.microsoft.com/aspnet/core/fundamentals/middleware

  • Endpoint / Minimal API / Controller — реализация Command, обрабатывающая конкретные маршруты.

  • Фильтры и middleware-декораторы — паттерн Decorator, расширяют функциональность без изменения логики компонентов.

  • Routing / Authorization — используют конфигурируемые политики и обработчики, которые можно заменять. Это похоже на Strategy, но официально не именуется так.

  • Model Binding / Data Mapping — преобразование данных запроса в объекты, интеграция с DI, отсутствие глобального состояния.

  • Context Object — HttpContext хранит состояние запроса, доступ к сервисам и ответу.

  • Proxy / Facade — Kestrel скрывает детали работы HTTP/TCP и упрощает работу приложения с сетью.

Полезные ссылки с официальной документацией Microsoft:

— Generic Host: https://learn.microsoft.com/dotnet/core/extensions/generic-host

— Dependency Injection: https://learn.microsoft.com/dotnet/core/extensions/dependency-injection

— Hosted services (IHostedService): https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services

— Options pattern: https://learn.microsoft.com/dotnet/core/extensions/options

— Configuration binding: https://learn.microsoft.com/dotnet/core/extensions/configuration

— Middleware: https://learn.microsoft.com/aspnet/core/fundamentals/middleware

— Minimal APIs: https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis

— Kestrel: https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel

— Launch settings: https://learn.microsoft.com/visualstudio/ide/run-debug-settings?view=vs-2022#launchsettingsjson

— HttpContext API: https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext