Эта статья о паттернах объектно‑ориентированного проектирования и архитектурных паттернах на платформе .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: