Перейти к основному содержимому

ASP.NET - фреймворк для веб-приложений

Разработчику Архитектору

Play ITЗагрузка интерактивного демо…


ASP.NET

См. также: Первая программа на ASP.NET Core · Minimal API и OpenAPI · FluentValidation, Polly и rate limiting · Razor Pages · Identity — JWT и cookie · MediatR и pipeline · тесты ASP.NET · Blazor · .NET MAUI · EF Core — первая программа · EF Core — продвинутое · Clean Architecture на ASP.NET Core · Выбор архитектуры под сценарий · Справочник · Продвинутый пример с БД и S3 · Документация Microsoft Learn.


С чего начать новичку

Эта статья — обзор архитектуры ASP.NET Core (хостинг, middleware, MVC, конфигурация, DI). Если вы только входите в веб на C#, удобнее идти от практики к теории:

ШагСтатьяЧто получите
1Первая программа ASP.NET CoreHTTP, MapGet, Swagger, контроллер
2Minimal API и OpenAPIГруппы маршрутов, типы ответов
3Razor PagesHTML-формы с сервера
4EF Core или ADO.NET / DapperДанные в БД
5Identity — JWT и cookieAPI, MVC, роли
6Тесты — юнит и интеграцияMoq для MVC, WebApplicationFactory для API
7Blazor / MAUIUI в браузере или в сторе
8Эта статья (451)Как всё устроено внутри
Как читать этот материал

Сначала пройдите практические шаги из Первая программа на ASP.NET Core и Razor Pages — первая программа, затем возвращайтесь к архитектурным разделам ниже. Такой порядок упрощает понимание терминов и решений по ходу чтения.

Мини-словарь для чтения обзора:

ТерминОдной фразой
HTTPПротокол "запрос–ответ" между клиентом и сервером.
KestrelВстроенный веб-сервер .NET, принимает HTTP.
MiddlewareИнфраструктурная цепочка до маршрутизации: auth, логи, CORS.
Endpoint filterЛогика конкретного маршрута после routing (.NET 7+).
Policy pipelineЦепочка независимых бизнес-проверок перед handler.
EndpointКонечная точка: контроллер, Razor Page или MapGet.
DIКонтейнер подставляет сервисы в конструкторы.
RazorШаблоны HTML + C# (.cshtml, .razor).

Ниже — углублённый разбор эволюции ASP.NET, хостинга, pipeline, MVC, Razor Pages, Web API, Blazor, маршрутизации, шаблонов, конфигурации и работы с данными.


Официальная документация

Актуальный хаб ASP.NET Core 10 на русском: learn.microsoft.com — ASP.NET. Там же — выбор между Minimal API, Web API, Razor Pages, MVC и Blazor, разделы по Kestrel, безопасности и Docker для Core.

Курированный навигатор по хабу, сериям Razor Pages (RazorPagesMovie) и MVC (MvcMovie), legacy Docker для ASP.NET MVC на .NET Framework: Документация и практика ASP.NET. Интерактивно — навигатор Learn и подборка документации.

C# позволяет писать веб-приложения. Для этого направления есть целая технология ASP.NET.

Давайте погрузимся в веб-разработку.


1. Введение в веб-разработку на платформе .NET

Веб-разработка в экосистеме .NET — это последовательная эволюция архитектурных подходов, обусловленная изменением требований к приложениям — от монолитных, серверных, stateful систем к распределённым, stateless, API-ориентированным сервисам. Эта эволюция отражена в линейке технологий Microsoft: ASP.NET Web Forms → ASP.NET MVC → ASP.NET Web API → ASP.NET Core.

Web Forms (2002) стремился перенести модель событийного программирования Windows Forms в веб — с её PostBack’ами, ViewState и серверными контролами. Это позволяло разработчикам, привыкшим к desktop-разработке, быстро создавать интерактивные веб-страницы, но ценой чёрного ящика — сложная модель состояния, неявная генерация HTML, трудности в тестировании и масштабировании. Архитектура была page-centric: логика жила внутри страницы, а не в переиспользуемых компонентах.

ASP.NET MVC (2009) стал ответом на рост популярности фреймворков вроде Ruby on Rails и Django. Он ввёл явное разделение ответственности по шаблону Model-View-Controller, обеспечил полный контроль над HTML, упростил unit-тестирование и способствовал созданию чистых HTTP-интерфейсов. MVC не заменил Web Forms — он предложил альтернативную модель, ориентированную на разработчиков, мыслящих в терминах HTTP и REST.

Параллельно выросла потребность в интерфейсах для клиентских приложений (SPA, мобильные приложения). ASP.NET Web API (2012) был извлечён из MVC как отдельный стек для построения HTTP-based API — лёгких, сериализуемых, контрактных сервисов, возвращающих JSON или XML. Хотя Web API использовал тот же DI, маршрутизацию и фильтры, что и MVC, его семантика была иной: здесь не было View — только модели, контроллеры (ApiController) и сериализаторы.

Настоящий перелом произошёл с появлением ASP.NET Core (2016). Это не улучшенная версия ASP.NET — это переписанная с нуля платформа, не имеющая прямой зависимости от System.Web.dll, совместимой с .NET Framework. ASP.NET Core — кроссплатформенный, высокопроизводительный, модульный и open-source фреймворк, объединивший MVC, Web API и Razor Pages в единую модель. Он построен на новых принципах:

  • Явная конфигурация вместо магии (нет глобальных статических классов вроде HttpContext.Current),
  • Композиция через middleware, а не наследование и события,
  • Встроенный DI и конфигурация как первоклассные граждане,
  • Контракт хостинга, позволяющий запускать приложение в любом окружении: от IIS до Docker-контейнера на Linux.

Ключевое понимание: в ASP.NET Core нет "веб-приложения" как отдельной сущности. Есть хост, который управляет жизненным циклом приложения, и pipeline, обрабатывающий входящие запросы. Всё остальное — добавляемые компоненты.


2. ASP.NET Core — архитектура и хостинг

Ядро — Kestrel

Kestrel — это встроенный, кроссплатформенный, асинхронный веб-сервер на базе System.IO.Pipelines и System.Threading.Channels. Он является обязательной частью любого ASP.NET Core-приложения, даже если оно развёрнуто за IIS или Nginx.

Kestrel не позиционируется как полноценный edge-сервер для публичного доступа (хотя с .NET 7+ его безопасность и производительность позволяют использовать его напрямую в некоторых сценариях). Его основная задача — обеспечить единый контракт выполнения между приложением и хост-окружением. Независимо от того, запущено ли приложение в облаке, на локальной машине или в контейнере, Kestrel предоставляет один и тот же интерфейс: он принимает TCP-соединения, парсит HTTP/1.1 или HTTP/2, формирует объект HttpContext и передаёт его в pipeline.

Важно: Kestrel всегда работает как самостоятельный процесс. Даже при развёртывании в IIS он запускается внутри w3wp.exe, но не встраивается в него как модуль — он живёт как отдельный managed-поток, управляемый хостом.


Варианты развёртывания

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

  1. Standalone (self-hosted)
    Приложение компилируется как исполняемый файл (.exe на Windows, без расширения на Linux), который сам запускает Kestrel. Это реализуется через WebApplication.CreateBuilder().Build().Run(). Такой процесс может быть запущен напрямую из командной строки, через systemd (Linux), launchd (macOS) или Task Scheduler (Windows). Для production-развёртывания обычно используется reverse proxy (Nginx, Apache, HAProxy), который:

    • принимает внешние запросы,
    • обрабатывает TLS/SSL,
    • балансирует нагрузку,
    • защищает от DDoS и медленных клиентов,
    • передаёт запросы Kestrel по локальному сокету или HTTP.

    Это стандартный подход для Linux-хостинга и облачных сред (Azure App Service в Linux-режиме, AWS ECS, Kubernetes).

  2. Windows Service / Linux Daemon
    Для long-running background-приложений (например, внутренние API, интеграционные шлюзы), не требующих веб-интерфейса, ASP.NET Core позволяет зарегистрировать приложение как системную службу.

    • На Windows используется WindowsServiceLifetime, и приложение устанавливается через sc create.
    • На Linux — через systemd unit-файл с типом Type=notify и ExecStart=/path/to/app.
      Такой подход даёт автоматический перезапуск при падении, управление зависимостями ("запускать после сети"), журналирование через системные журналы (journald, Event Log).
      Kestrel в этом режиме может быть отключён (webBuilder.UseKestrel() => webBuilder.ConfigureKestrel(options => options.ListenLocalhost(0)) или вообще не вызываться), если приложение не слушает HTTP-порты.
  3. Интеграция с IIS (In-Process и Out-of-Process)

IIS остаётся важной платформой для enterprise-развёртываний на Windows. ASP.NET Core поддерживает два режима работы под IIS:

  • Out-of-Process (режим по умолчанию до .NET Core 3.0)
    IIS выступает как reverse proxy. Модуль ASP.NET Core Module (ANCM) перехватывает запрос, запускает отдельный процесс dotnet.exe, в котором работает Kestrel, и перенаправляет трафик через named pipe. Процесс управляется IIS: запускается при первом запросе, останавливается при простое.
    Преимущества: изоляция, совместимость со старыми IIS-модулями.
    Недостатки: накладные расходы на межпроцессное взаимодействие.

  • In-Process (начиная с .NET Core 3.0)
    ANCM загружает .NET Core Runtime непосредственно в рабочий процесс IIS (w3wp.exe). Kestrel не используется — вместо него ASP.NET Core использует IIS HTTP Server, который напрямую взаимодействует с HTTP.sys через IIS.
    Это даёт до 2–3× прирост производительности за счёт устранения IPC, но:

    • приложение должно быть framework-dependent (не self-contained),
    • требуется совместимость с модулями IIS (например, URL Rewrite работает, но некоторые legacy-модули — нет),
    • все приложения в Application Pool должны использовать одну и ту же версию .NET.

Как это работает на уровне Windows?

Когда IIS принимает HTTP-запрос, он не обрабатывает его напрямую. За это отвечает Windows Process Activation Service (WAS) — ядро хостинга, появившееся в IIS 7. WAS управляет жизненным циклом Application Pools и рабочих процессов.

  • World Wide Web Publishing Service (WWW Service) — это компонент WAS, отвечающий именно за HTTP/HTTPS. Он читает конфигурацию из applicationHost.config (глобальный файл настройки IIS, обычно в %windir%\System32\inetsrv\config), создаёт Application Pools и привязывает их к сайтам.
  • Каждый Application Pool — это изолированное окружение, в котором работает один или несколько сайтов. У Pool’а есть свои настройки: версия .NET, режим pipeline (Integrated/Classic), учётная запись, limits (CPU, memory, requests).
  • Рабочий процесс — это w3wp.exe (Worker Process). Каждый Pool может иметь один или несколько таких процессов (Web Garden). w3wp.exe загружает модули IIS, включая aspnetcorev2_inprocess.dll (для In-Process) или aspnetcorev2_outofprocess.dll (для Out-of-Process).
  • svchost.exe — это общий хост для Windows-сервисов. WAS и WWW Service работают внутри svchost.exe (можно увидеть в Process Explorer: svchost.exe -k iissvcs). Это важно понимать при диагностике: сбой в WAS может повлиять на все сайты сервера.

Типы развёртывания — self-contained и framework-dependent

  • Framework-dependent deployment (FDD)
    Приложение компилируется только в IL-код и метаданные. Для запуска требуется предустановленный .NET runtime той же (или совместимой) версии. Это уменьшает размер дистрибутива, упрощает обновление runtime, но требует контроля над окружением. Используется в IIS In-Process, в shared hosting и при централизованном управлении.

  • Self-contained deployment (SCD)
    Приложение поставляется вместе со всей необходимой частью .NET runtime. Это позволяет запускать его на "чистой" машине, использовать специфическую версию (например, с патчем), изолироваться от глобальных обновлений. Размер увеличивается на ~100–150 МБ, но зато нет внешних зависимостей. Используется в standalone-режиме, в контейнерах, в offline-средах.


Модели хостинга — shared, on-premise, cloud

  • Shared hosting — устаревшая модель, где множество сайтов делят один Application Pool и ресурсы сервера. В ASP.NET Core почти не используется — нарушает изоляцию, мешает настройке, несовместима с SCD.

  • On-premise — развёртывание в собственном дата-центре. Здесь возможны все варианты — IIS (In/Out-of-Process), Windows Service с reverse-proxy, bare-metal Kestrel. Требует управления инфраструктурой, но даёт полный контроль.

  • Cloud — абстрагированная среда (Azure App Service, AWS Elastic Beanstalk, Google Cloud Run). Платформа управляет масштабированием, балансировкой, обновлениями. В Azure App Service, например, Linux-вариант использует standalone + Docker + Nginx, Windows-вариант — IIS In-Process. Cloud-провайдеры предоставляют встроенные health-checks, logging, monitoring — но требуют адаптации приложения (например, stateless-дизайн, externalised config).


3. Конвейер обработки HTTP-запроса

Как выбрать модель развёртывания в реальном проекте

СценарийБазовый выборЧто учесть
Внутренний сервис на LinuxStandalone + reverse proxyЛоги, health checks, systemd
Корпоративная Windows-инфраструктураIIS In-ProcessЦентрализованное администрирование IIS
Контейнерная платформа (Kubernetes)Standalone/SCD в контейнереStateless-дизайн и внешняя конфигурация
Изолированная среда без общего runtimeSCDБольше размер артефакта

Такой выбор обычно фиксируют в архитектурном решении проекта и пересматривают при росте нагрузки.


HttpContext — контекст жизни запроса

Каждый HTTP-запрос, поступающий в Kestrel, инкапсулируется в объект Microsoft.AspNetCore.Http.HttpContext. Он — единственный источник истины о текущем запросе и ответе. Именно через него middleware и компоненты приложения взаимодействуют с клиентом.

HttpContext не является простым DTO. Это активный объект, связывающий:

  • входящий запрос (HttpRequest — метод, URL, заголовки, тело, куки),
  • исходящий ответ (HttpResponse — статус, заголовки, тело),
  • состояние аутентификации (User, AuthenticationFeature),
  • сессию (Items — dictionary для передачи данных между middleware),
  • DI-контейнер текущего request scope (RequestServices),
  • вспомогательные сервисы (Features — расширяемая коллекция интерфейсов, например, IHttpConnectionFeature, IHttpRequestIdentifierFeature).

Важно: HttpContext создаётся один раз на запрос и уничтожается после отправки ответа. Его нельзя хранить в статических полях, кэшировать или передавать в фоновые потоки — это приведёт к неопределённому поведению или утечкам памяти. Для асинхронной обработки следует использовать RequestDelegate или IHostedService с явной передачей данных.


Middleware — компоненты конвейера

Middleware (промежуточное ПО) — это функция, которая получает HttpContext, может выполнить произвольную логику до и после передачи управления следующему middleware, и, при необходимости, прервать цепочку.

Формально middleware — это делегат вида RequestDelegate, но на практике реализуется как класс с методом Invoke или InvokeAsync, принимающим HttpContext и RequestDelegate next. Порядок регистрации middleware в Program.cs определяет порядок их выполнения — это архитектурное решение.

Три ключевых метода расширения для построения pipeline:

  • app.Use(Func<HttpContext, Func<Task>, Task> middleware)
    Регистрирует не терминальный middleware. Он всегда вызывает next(), передавая управление дальше, и может выполнять код как до, так и после этого вызова. Пример — логирование:
app.Use(async (context, next) =>
{
var start = Stopwatch.GetTimestamp();
await next(); // выполнение следующих middleware и конечной точки
var duration = Stopwatch.GetElapsedTime(start);
logger.LogRequest(context.Request.Path, duration);
});

Разбор:

  • app.Use(...) добавляет middleware-обёртку, которая выполняется до и после следующего звена.

  • await next() передаёт запрос дальше по цепочке и ждёт завершения внутренних обработчиков.

  • Stopwatch.GetTimestamp() и GetElapsedTime(...) измеряют длительность обработки запроса.

  • logger.LogRequest(...) записывает путь и время ответа в журнал.

    Такой middleware формирует "обёртку" (wrapper), подобную try-finally.

  • app.Run(Func<HttpContext, Task> middleware)
    Регистрирует терминальный middleware. Он не вызывает next() — вместо этого сам формирует ответ и завершает pipeline. Пример — fallback-обработчик:

app.Run(context =>
{
context.Response.StatusCode = 404;
return context.Response.WriteAsync("Not Found");
});

Разбор:

  • app.Run(...) регистрирует терминальный middleware, который завершает обработку.

  • StatusCode = 404 задаёт HTTP-код "ресурс не найден".

  • WriteAsync("Not Found") пишет текстовое тело ответа клиенту.

  • Этот обработчик обычно используют как fallback в конце pipeline.

    После Run ничего не выполняется. Поэтому Run обычно ставится в самый конец.

  • app.Map(string pathMatch, Action<IApplicationBuilder> configuration)
    Условно-ветвящий middleware. Если путь запроса начинается с pathMatch, создаётся вложенный pipeline, в котором выполняются middleware из configuration. Это позволяет изолировать логику для отдельных подсистем (например, /api и /admin).
    Пример:

app.Map("/health", healthApp =>
{
healthApp.Run(context => context.Response.WriteAsync("OK"));
});

Разбор:

  • app.Map("/health", ...) создаёт отдельную ветку pipeline для конкретного пути.

  • healthApp.Run(...) возвращает ответ "OK" и завершает обработку запроса.

  • Ветка изолирует технический endpoint и не смешивает его с бизнес-маршрутами.

  • При совпадении пути основной pipeline не продолжает выполнение.

    Внутри healthApp можно использовать Use, Run, Map — это полноценный IApplicationBuilder.

Существуют также MapWhen (ветвление по условию, например, context.Request.Headers.ContainsKey("X-Internal")) и UseWhen (условное подключение middleware без ветвления всего pipeline).


Принцип работы конвейера

Pipeline — это линейная цепочка вызовов, реализованная через композицию делегатов. При вызове app.Build() фреймворк рекурсивно оборачивает каждый middleware в замыкание, где next указывает на следующий в цепочке.

Логически выполнение выглядит так:

[Client]
↓ HTTP Request
[Kestrel] → создаёт HttpContext

[Middleware 1] → Before next()

[Middleware 2] → Before next()

...

[Middleware N] → Before next()

[Endpoint] → выполнение контроллера / Razor Page / делегата

[Middleware N] → After next()

...

[Middleware 2] → After next()

[Middleware 1] → After next()

[Kestrel] → отправка ответа

[Client]

Разбор:

  • Схема показывает "входящий" и "исходящий" проход по цепочке middleware.
  • Блоки Before next() выполняются при движении к endpoint, After next() — при возврате ответа.
  • Такой порядок объясняет, почему верхние middleware могут логировать итоговый статус и длительность.
  • Если какое-то звено не вызывает next(), нижние middleware и endpoint не запускаются.

Если какой-либо middleware не вызывает next(), цепочка прерывается, и управление возвращается вверх по стеку — только те middleware, которые уже вызвали next(), выполнят свой "after"-код. Это позволяет реализовывать short-circuiting — например, middleware аутентификации может сразу вернуть 401, не передавая запрос дальше.


Семантика порядка middleware

Порядок критичен. Вот рекомендуемая последовательность (с обоснованием):

  1. UseExceptionHandler / UseDeveloperExceptionPage
    Самый первый — чтобы перехватывать исключения на всех уровнях.

  2. UseHttpsRedirection
    Раннее перенаправление с HTTP на HTTPS — до любой бизнес-логики.

  3. UseStaticFiles
    Обслуживание wwwroot — если запрос совпадает с файлом, pipeline завершается здесь (это Run внутри).

  4. UseRouting
    Не обрабатывает запрос, а только определяет endpoint. После него становятся доступны данные маршрутизации (context.GetEndpoint()), но endpoint ещё не вызван.

  5. UseAuthentication
    Определяет context.User — должен быть до авторизации и бизнес-логики.

  6. UseAuthorization
    Проверяет права на основе User и политик — после аутентификации, до вызова endpoint.

  7. UseSession
    Требует User для привязки сессии — после авторизации.

  8. UseEndpoints / MapRazorPages / MapControllers
    Вызывает endpoint (контроллер, страницу и т.д.). Это терминальный middleware для основного потока.

  9. Run (fallback)
    Обработка 404 — в самом конце.

Нарушение этого порядка ведёт к ошибкам — например, если UseAuthorization поставить до UseAuthentication, User будет null, и все проверки провалятся.

Мини-памятка порядка в Program.cs:

app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

Разбор:

  • Последовательность вызовов определяет порядок обработки запроса в приложении.
  • UseRouting() подбирает endpoint, после чего становятся доступны его метаданные.
  • UseAuthentication() заполняет пользователя в HttpContext, UseAuthorization() проверяет доступ.
  • MapControllers() подключает конечные обработчики контроллеров.
  • Нарушение порядка часто приводит к ошибкам авторизации и неожиданным 404/401.

Где жить логике: Middleware, Endpoint Filters, Policy Pipeline

В зрелом API легко накопить десятки проверок — аутентификация, лимиты, валидация DTO, торговые часы, регион, feature flags, регуляторные ограничения. Если всё свалить в один handler или в случайные middleware, поддержка превращается в "зоопарк if". В ASP.NET Core удобно мыслить тремя слоями ответственности — от общего к частному.

Упрощённый путь запроса:

Request → Middleware → Routing → Endpoint Filter → Policy Pipeline → Handler
СлойГлавный вопросКритерий выбора
Middleware"Что нужно всем запросам?"Правило должно работать для любого URL и любого endpoint.
Endpoint filter"Что зависит от этого маршрута?"Нужны endpoint, параметры handler, результат до/после вызова.
Policy pipeline"Какие бизнес- и регуляторные правила перед сценарием?"Набор переиспользуемых проверок домена; каждая — одна ответственность.

Чем раньше появляется такое разделение, тем проще жить через год, когда вместо трёх проверок станет тридцать.


Middleware — для инфраструктуры

Middleware работает для каждого запроса и выполняется до бизнес-логики endpoint (см. разделы выше про конвейер и порядок Use*).

Подходит для кросс-функциональной инфраструктуры:

ЗадачаПочему middleware
Аутентификация (UseAuthentication)Нужен User до любого handler.
Авторизация на уровне схем (UseAuthorization)Единые политики доступа к маршрутам.
Логирование, correlation ID, tracingОдинаково для API, статики, health.
Rate limiting, глобальный exception handlerНе привязано к конкретному действию.
HTTPS, CORS, сжатие, статикаТранспорт и окружение, не домен.

Главный критерий: если правило должно сработать независимо от того, какой endpoint выбран (или ещё не выбран) — это middleware. Middleware видит "голый" HttpContext — путь, заголовки, тело, но не знает сигнатуру метода контроллера и не должен разбирать бизнес-DTO.

Типичная ошибка — класть в middleware проверки вроде "рынок открыт для этой операции" или "клиент может покупать облигации": для этого уже известен конкретный сценарий и модель запроса.


Endpoint Filters — для логики конкретного endpoint

Endpoint filters появились в .NET 7 для Minimal API (IEndpointFilter, AddEndpointFilter). В MVC и Razor Pages давно есть родственники — action filters и page filters; идея та же: выполнение после routing, когда endpoint уже известен.

Фильтр "знает" больше, чем middleware:

  • какой endpoint вызван (метаданные маршрута, имя handler);
  • какие параметры привязаны к handler (в т.ч. из body и route);
  • может обернуть вызов handler и изменить или заменить результат (EndpointFilterInvocationContext).

Подходит для:

ЗадачаПример
Валидация входа конкретного маршрутаОбязательные поля POST /api/orders до handler.
Feature flags на группу APIMapGroup("/beta").AddEndpointFilter(...)
Обогащение контекстаПоложить tenant id в HttpContext.Items только для /api/...
ПредвыполнениеАудит, метрики, проверка API-ключа на одной группе маршрутов

Цепочка: Request → Middleware → Routing → Endpoint Filter → Handler (для Minimal API; в MVC фильтры встраиваются в вызов action внутри endpoint).

Практика: Minimal API — endpoint filters. Справочник по AddEndpointFilter: Справочник по ASP.NET.

Не путать с authorization policies в ASP.NET ([Authorize(Policy = "CanTrade")]) — это встроенный механизм claims/ролей, часто настраивается через middleware авторизации. Endpoint filter — ваш код вокруг handler, policies — декларативные правила доступа.


Policy Pipeline — когда бизнес-правил много

Policy pipelineархитектурный приём — цепочка независимых бизнес-проверок, каждая отвечает за одно правило и может остановить выполнение с понятной ошибкой (403, 422, доменное исключение).

Пример финансового API перед операцией "купить облигации":

  1. права клиента на операцию;
  2. регуляторные ограничения;
  3. торговые часы;
  4. региональные ограничения;
  5. feature flag продукта.

Запихивать все if в handler неудобно: handler должен описывать сценарий ("создать заявку"), а не каталог регуляций.

ПринципСмысл
Одна policy — одна проверкаITradingHoursPolicy, IRegionPolicy — отдельные классы.
Порядок явныйРегистрация цепочки в DI или композиция в Application.
Остановка на первом отказеНе вызывать handler, если policy вернула failure.
ПереиспользованиеТе же policies в Minimal API, MVC и фоновых job.

Где реализовать в .NET:

ПодходКогда уместен
Ручная цепочка IPolicy<TContext> + runnerЯвный контроль, мало зависимостей.
MediatR IPipelineBehaviorClean Architecture: проверки вокруг IRequest в Application — см. MediatR и pipeline в слое Application, Clean Architecture на ASP.NET Core.
FluentValidation в MediatR pipelineВалидация формы запроса (обязательные поля, форматы).
Отдельные domain servicesСложные правила с доступом к БД — policy вызывает сервис, не дублирует SQL.

Policy pipeline vs endpoint filter: filter привязан к HTTP-слою (маршрут, binding, ответ API). Policy pipeline — к use-case ("можно ли выполнить команду BuyBonds") и живёт в Application даже если завтра тот же сценарий вызовут не из HTTP, а из очереди.

Policy pipeline vs MediatR: MediatR pipeline — удобная реализация policy pipeline для команд/запросов; термин "policy pipeline" описывает задачу (бизнес-цепочка), MediatR — один из инструментов.


Простое правило выбора
Если…То…
логика касается всех запросовMiddleware
логика зависит от конкретного endpoint (маршрут, параметры handler)Endpoint filter (или MVC/Razor filter)
это набор переиспользуемых бизнес- или регуляторных правил перед сценариемPolicy pipeline (часто MediatR behaviours + domain policies)
это только "поля заполнены и типы верны"Валидация модели / FluentValidation — ближе к filter или MediatR validation behaviour
это "кто пользователь и есть ли роль Admin"Authentication + authorization policies, не бизнес-policy pipeline

Антипаттерны:

  • Middleware, который парсит body конкретного DTO — признак того, что логику пора опустить в filter или Application.
  • Handler на 200 строк с if по регуляциям — вынести в policy pipeline.
  • Дублировать одну и ту же бизнес-проверку в контроллере, Minimal API и background worker — оформить как policy в Application.

Middleware и классические MVC/Razor Filters

Помимо endpoint filters, в ASP.NET Core остаются фильтры MVC и Razor Pages — та же идея "после routing, внутри endpoint", но с другими контекстами:

  • Middleware — уровень всего приложения, "голый" HttpContext — логирование, CORS, сжатие, глобальные ошибки.
  • Фильтры MVC/RazorActionExecutingContext, PageHandlerExecutingContext — параметры action, ModelState, результат, DI контроллера.

Middleware — внешняя оболочка HTTP; фильтры и policy pipeline — внутренняя инструментовка вокруг handler и use-case. Слои дополняют друг друга, а не заменяют.

Типы фильтров MVC (IAuthorizationFilter, IActionFilter, IExceptionFilter) — в документации Microsoft. Логику action удобно проверять юнит-тестами с Moq; весь HTTP-pipeline — Тесты ASP.NET Core — юнит и интеграция.

Пример policy в Application (упрощённо):

Код ITЗагрузка примера кода…

Разбор:

  • Каждая IPolicy инкапсулирует одно бизнес-правило и возвращает успех или причину отказа.
  • PolicyPipeline задаёт порядок и останавливается на первом неуспехе — handler не вызывается.
  • TContext несёт данные сценария (команда, пользователь, продукт), а не HTTP-детали.
  • Такую цепочку можно вызывать из MediatR behaviour, endpoint filter (тонкая обёртка) или worker.

Сравнение с OWIN

OWIN (Open Web Interface for .NET) — спецификация 2010-х годов, определявшая минимальный контракт между веб-сервером и приложением — Func<IDictionary<string, object>, Task>. ASP.NET Core изначально планировался как реализация OWIN, но в итоге от него отошёл.

Почему? OWIN слишком низкоуровнев:

  • Передача данных через IDictionary — нет типизации, легко ошибиться в именах ключей.
  • Нет поддержки DI, конфигурации, логирования "из коробки".
  • Middleware не могли зависеть от порядка инициализации.

ASP.NET Core сохранил идею композиции, но заменил словарь на строго типизированный HttpContext, а OWIN-совместимость вынес в отдельный пакет (Microsoft.AspNetCore.Owin), который оборачивает ASP.NET Core в OWIN-интерфейс — но не наоборот.

Сегодня OWIN используется только для совместимости с legacy-библиотеками (например, некоторыми OAuth-провайдерами). Для нового кода применяется нативный pipeline.


4. Модели разработки веб-приложений

Model-View-Controller (MVC)

MVC — это архитектурный шаблон, адаптированный для веб. В ASP.NET Core он реализован как полноценная модель разработки с чётким разделением:

  • Model — классы данных (DTO, domain entities) и логика предметной области — валидация, преобразования, взаимодействие с репозиториями. Model отвечает на вопрос "что?" — какие данные нужны и как они связаны.

  • View — строго типизированный шаблон (.cshtml), отвечающий за визуальное представление. View знает только о своей Model (через @model), не содержит бизнес-логики и не обращается к сервисам напрямую. Её задача — преобразовать данные в HTML. Важно: View не управляет состоянием — она реактивна.

  • Controller — координатор. Он принимает HTTP-запрос, извлекает данные (из query, body, route), вызывает сервисы для подготовки Model, выбирает View и передаёт ей данные. Controller отвечает на вопрос "как?" — как обработать запрос, но не "почему?" — это прерогатива сервисов.

Жизненный цикл вызова в MVC:

  1. Middleware UseRouting() сопоставляет URL с шаблоном маршрута (например, {controller=Home}/{action=Index}/{id?}).
  2. Middleware UseEndpoints() создаёт экземпляр контроллера через DI.
  3. Model Binding заполняет параметры действия (action method) из источников:
    • [FromRoute] — из сегментов URL (/product/123id = 123),
    • [FromQuery] — из строки запроса (?page=2),
    • [FromBody] — из тела (JSON/XML),
    • [FromForm] — из multipart/form-data.
  4. Выполняются фильтры (filters). Для MVC типы Action filters (и соседние стадии):
    • Authorization filters[Authorize], политики;
    • Resource filters — кэш, короткое замыкание до action;
    • Action filters — валидация модели, логирование, [ServiceFilter];
    • Exception filters — обработка исключений на уровне action (реже в Core 3+);
    • Result filters — постобработка ответа (кэш, формат). В Minimal API аналог — endpoint filters (IEndpointFilter).
  5. Вызывается метод контроллера (action). Он может:
    • вернуть View() с Model (рендеринг страницы),
    • вернуть Json(), Ok(), CreatedAtAction() (Web API-стиль),
    • перенаправить (RedirectToAction()).
  6. Если возвращён ViewResult, запускается View Engine (Razor), который компилирует .cshtml в C#-код, выполняет его и генерирует HTML.
  7. Выполняются Result Filters (например, кэширование ответа).

Когда применять MVC?
— Когда нужно гибкое управление маршрутизацией (RESTful API + HTML-страницы в одном приложении),
— Когда View и Controller разрабатываются разными командами (фронтенд и бэкенд),
— Когда требуется сложная композиция UI (View Components, частичные представления),
— В enterprise-приложениях с многоуровневой архитектурой (Presentation → Application → Domain → Infrastructure).


Razor Pages

Razor Pages — это page-centric модель, появившаяся в ASP.NET Core 2.0 как ответ на потребность в более простой альтернативе MVC для CRUD-сценариев и внутренних инструментов (админки, панели управления).

  • Page Model — класс с расширением .cshtml.cs, унаследованный от PageModel. Он объединяет логику обработки и данные для отображения. Свойства помечаются атрибутами привязки ([BindProperty]), методы — OnGet(), OnPost(), OnPostAsync().

  • Page — файл .cshtml, который:

    • обязательно начинается с @page,
    • привязан к Page Model через @model или по соглашению имён,
    • может содержать обработчики в @functions, но это не рекомендуется.

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

КритерийMVCRazor Pages
Единица разработкиКонтроллер + ViewPage Model + Page
МаршрутизацияГлобальные шаблоны или атрибуты на контроллереАвтоматическая по пути файла (например, /Pages/Admin/Users/Index.cshtml/Admin/Users/Index)
СостояниеНет встроенного механизмаModelState, TempData, ViewData работают так же, но проще управлять в рамках одной страницы
СложностьВыше (разделение на 3 части)Ниже (всё в одном месте)
ТестированиеКонтроллеры легко тестируются изолированноPage Model тестируется как обычный класс, но с зависимостью от PageContext

Жизненный цикл вызова в Razor Pages:

  1. UseRouting() сопоставляет URL с файлом страницы (по соглашению).
  2. UseEndpoints() создаёт экземпляр Page Model через DI.
  3. Выполняются Page Filters (аналог Action Filters).
  4. Вызывается метод-обработчик (OnGet(), OnPost() и т.д.).
    • Привязка модели происходит автоматически для свойств с [BindProperty] (по умолчанию — только для POST).
    • Можно использовать [BindProperty(SupportsGet = true)] для GET-параметров.
  5. Если метод возвращает Page(), запускается Razor Engine.
  6. Выполняются Result Filters.

Когда применять Razor Pages?
— Для внутренних инструментов (админки, отчёты), где важна скорость разработки,
— Когда страница автономна (редко переиспользуется логика между страницами),
— В образовательных проектах — проще объяснить "страница = файл + код".

Важно: Razor Pages не заменяет MVC. Это альтернатива для других сценариев. Проект может сочетать обе модели: Razor Pages для админки, MVC — для публичного API.


Web API

Web API — это подход к построению HTTP-based интерфейсов, ориентированных на программное потребление (SPA, мобильные приложения, микросервисы). В ASP.NET Core Web API не выделен в отдельный фреймворк — он интегрирован в MVC.

Ключевые признаки Web API-контроллера:

  • Наследуется от ControllerBase (не от Controller, чтобы избежать View-функциональности),
  • Помечен атрибутом [ApiController],
  • Методы возвращают IActionResult (или напрямую объект, который сериализуется в JSON/XML).

Атрибут [ApiController] включает важные соглашения по умолчанию:

  • Автоматическая привязка из тела для сложных типов (без [FromBody]),
  • Автоматический ответ 400 при !ModelState.IsValid,
  • Требование явных атрибутов маршрутизации ([Route], [HttpGet]),
  • Интерпретация параметров как обязательных, если нет ? или значения по умолчанию.

Два стиля построения API:

  1. REST-like (ресурс-ориентированный)
    Основан на концепции ресурса (например, /api/users/123). Операции выражаются через HTTP-методы:
    • GET /users → список,
    • GET /users/123 → получение,
    • POST /users → создание,
    • PUT /users/123 → полное обновление,
    • PATCH /users/123 → частичное обновление,
    • → удаление.
DELETE /users/123

Разбор:

  • Пример показывает удаление ресурса с идентификатором 123.
  • HTTP-метод DELETE выражает намерение удалить сущность по указанному URL.
  • Типичные успешные ответы для такого запроса: 204 No Content или 200 OK. Преимущества — предсказуемость, кэшируемость (GET), совместимость с инструментами (Swagger, Postman).
    Недостатки: сложно выразить сложные операции ("перевести деньги"), требует HATEOAS для навигации.
  1. RPC-like (операция-ориентированный)
    URL выражает действие: /api/transactions/transfer, /api/reports/generate. Тело запроса содержит все параметры.
    Преимущества: естественно для бизнес-операций, легко версионировать.
    Недостатки — нарушает семантику HTTP, труднее кэшировать, менее стандартизировано.

На практике большинство API — гибрид: ресурсы для CRUD, RPC — для сложных операций.

Когда применять Web API?
— При создании бэкенда для SPA (React, Angular, Vue),
— При построении микросервисов,
— Для интеграции с внешними системами (B2B API),
— В сценариях, где клиент управляет UI (а сервер — только данными и логикой).


ASP.NET Web Forms — архитектурное наследие

Web Forms (2002) — это модель, имитирующая событийное программирование Windows Forms в вебе. Её ключевые концепции существенно отличаются от современных подходов и требуют отдельного объяснения — для сопровождения legacy-систем.

  • Страница (Page) — это класс, унаследованный от System.Web.UI.Page. Каждый .aspx-файл компилируется в такой класс при первом запросе.

  • Жизненный цикл страницы — строго определённая последовательность событий, через которые проходит Page при обработке запроса:
    PreInit → Init → InitComplete → LoadViewState → LoadPostData → PreLoad → Load → LoadComplete → PreRender → PreRenderComplete → SaveStateComplete → Render → Unload.
    Разработчик может подписаться на любое событие и выполнять код. Например, инициализация динамических контролов — в Init, привязка данных — в Load, финальные правки — в PreRender.

  • PostBack — механизм отправки формы на ту же страницу. При нажатии кнопки (<asp:Button>) генерируется JavaScript-вызов __doPostBack(), который отправляет POST-запрос с данными формы и __VIEWSTATE.
    Это позволяет сохранять состояние элементов управления без перезагрузки всей страницы (в связке с UpdatePanel — частичный PostBack через AJAX).

  • ViewState — механизм сериализации состояния контролов в скрытое поле __VIEWSTATE. При PostBack сервер десериализует его и восстанавливает состояние (значения TextBox, выбор в DropDownList и т.д.). Это скрывает stateless-природу HTTP, но увеличивает объём трафика и уязвим к подделке (требует machineKey для защиты).

  • Дерево элементов управления (Control Tree) — иерархическая структура, где каждый элемент (Label, Button, GridView) — это объект с собственным жизненным циклом. При рендеринге каждый контрол вызывает Render(), формируя HTML.

Почему Web Forms устарел?
— Сложность тестирования (сильная связность с HttpContext),
— Неэффективность (избыточный ViewState, тяжёлый HTML),
— Отсутствие контроля над генерируемым HTML,
— Несовместимость с современными фронтенд-фреймворками,
— Зависимость от IIS и Windows.

Однако многие enterprise-системы (1C, SAP, внутренние ERP) до сих пор используют Web Forms — поэтому понимание его архитектуры необходимо для миграции или интеграции.


Blazor — SPA на .NET

Blazor — фреймворк для SPA на C# и Razor: UI-логика пишется на .NET, а не на TypeScript. Тем не менее в браузере по-прежнему работает JavaScript/WebAssembly; для interop, библиотек и части экосистемы JS может понадобиться. Два режима хостинга:

  1. Blazor Server

    • UI-логика (обработчики событий, компоненты) выполняется на сервере,
    • Взаимодействие с браузером — через SignalR-соединение (WebSockets или long polling),
    • Состояние компонентов хранится на сервере (в памяти),
    • Преимущества: высокая производительность сервера, доступ к полному .NET API,
    • Недостатки — задержки при высокой latency, масштабирование требует sticky sessions, уязвимость к потере соединения.
  2. Blazor WebAssembly (WASM)

    • Вся логика компилируется в WebAssembly и выполняется в браузере,
    • Для доступа к данным — HTTP-запросы к API (обычно ASP.NET Core Web API),
    • Преимущества — offline-возможности, масштабирование (статический хостинг),
    • Недостатки — начальная загрузка (2–5 МБ), ограничения WASM (нет доступа к файловой системе, ограниченный .NET API), сложность отладки.

Компонентная модель Blazor:
— Компонент — это класс с расширением .razor, содержащий HTML-разметку и C#-логику,
— Взаимодействие через параметры ([Parameter]) и события (EventCallback),
— Жизненный цикл — OnInitialized, OnParametersSet, OnAfterRender,
— Состояние управляется через StateHasChanged() или INotifyPropertyChanged.

Когда применять Blazor?
— Blazor Server — для внутренних инструментов с контролируемой сетью (офисные приложения),
— Blazor WASM — для public-приложений, где важна клиентская производительность после загрузки,
— В командах, где нет экспертизы по JavaScript, но есть сильные .NET-разработчики.

WebAssembly (WASM) — бинарный формат для выполнения кода в браузере. Он дополняет JavaScript: WASM-модуль может вызывать JS и наоборот. В .NET WASM-файл содержит IL-код, который интерпретируется Mono runtime, скомпилированным в WASM.


5. Маршрутизация

Концепция endpoint

Endpoint — это логическая точка входа в приложение — метод контроллера, Razor Page, делегат RequestDelegate, gRPC-сервис. Каждый endpoint имеет:

  • Шаблон маршрута (например, api/[controller]/[action]),
  • Метаданные (атрибуты: [Authorize], [ResponseCache]),
  • Делегат обработки (RequestDelegate).

Маршрутизация в ASP.NET Core разделяется на два этапа:

  1. Регистрация endpoint’ов — происходит при запуске приложения (в Program.cs).
    Например:
endpoints.MapControllers(); // регистрирует все ApiController’ы
endpoints.MapRazorPages(); // регистрирует все Razor Pages
endpoints.MapGet("/ping", () => "OK"); // регистрирует делегат

Разбор:

  • MapControllers() включает endpoint-ы всех контроллеров Web API и MVC.
  • MapRazorPages() подключает страницы Razor в общую таблицу маршрутов.
  • MapGet("/ping", ...) добавляет минимальный endpoint без контроллера.
  • Регистрация выполняется на старте, а затем используется на каждом запросе во время роутинга.
  1. Сопоставление запроса с endpoint’ом — происходит на каждом запросе в middleware UseRouting().
    Оно определяет, какой endpoint соответствует запросу, и сохраняет его в HttpContext.GetEndpoint().

Только после этого middleware UseEndpoints() (или Map*) вызывает делегат endpoint’а.

Такое разделение позволяет middleware, зарегистрированным между UseRouting() и UseEndpoints(), получать доступ к метаданным endpoint’а (например, проверить, требует ли он авторизации), не запуская его логику.


Соглашения и атрибуты

ASP.NET Core поддерживает два способа определения маршрутов:

  1. Маршрутизация на основе соглашений
    Глобальные шаблоны, задаваемые при регистрации endpoint’ов. Пример:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);

Разбор:

  • MapControllerRoute(...) задаёт конвенционную схему URL для MVC-контроллеров.

  • controller=Home и action=Index подставляются, если сегменты отсутствуют в URL.

  • id? означает, что параметр может присутствовать или отсутствовать.

  • Такой маршрут покрывает стандартный формат /{controller}/{action}/{id}.

    • controller, action, id — параметры маршрута,
    • Home, Index — значения по умолчанию,
    • ? — параметр необязательный.

    Преимущества: единообразие, централизованное управление.
    Недостатки: сложно выразить сложные сценарии (версионирование, многоязычные URL).

  1. Маршрутизация на основе атрибутов
    Маршруты задаются непосредственно на контроллерах и методах:
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")] // шаблон на уровне контроллера
public class ProductsController : ControllerBase
{
[HttpGet("{id:int}")] // шаблон на уровне метода
public IActionResult Get(int id) { ... }
}

Разбор:

  • [ApiController] активирует соглашения Web API: строгую валидацию и упрощённую привязку параметров.

  • [Route(...)] на классе задаёт общий префикс URL для всех методов контроллера.

  • {version:apiVersion} и {id:int} добавляют ограничение типа и контроль формата сегментов.

  • [HttpGet("{id:int}")] связывает метод Get с запросом GET по маршруту с числовым id.

    • [controller], [action] — токены, заменяющиеся на имя класса/метода,
    • {id:int} — параметр с ограничением (только целые числа),
    • v{version:apiVersion} — параметр с кастомным ограничением (ApiVersionConstraint).

    Преимущества — точный контроль, поддержка REST, версионирование.
    Недостатки: дублирование, сложность аудита всех маршрутов.

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


Параметры маршрутов и ограничения

Параметры маршрута — это сегменты URL, заключённые в фигурные скобки: {parameter}. Они могут иметь:

  • Значения по умолчанию: {id=0} или через = defaultValue в шаблоне,
  • Необязательность: {id?} — сегмент может отсутствовать,
  • Ограничения (constraints) — встроенные или кастомные правила валидации.

Встроенные ограничения:

  • int, long, guid, datetime — типы,
  • min(1), max(100), range(1,100) — числовые диапазоны,
  • alpha, regex(...), length(5,10) — строковые правила,
  • booltrue/false,
  • apiVersion — для библиотеки Microsoft.AspNetCore.Mvc.Versioning.

Пример с несколькими ограничениями:

[HttpGet("report/{year:int:min(2000):max(2050)}/{month:range(1,12)}")]
public IActionResult GetReport(int year, int month) { ... }

Разбор:

  • В маршруте применены сразу несколько constraints для входных параметров.
  • year:int:min(2000):max(2050) допускает только годы в заданном диапазоне.
  • month:range(1,12) валидирует месяц до вызова метода контроллера.
  • Такой подход убирает часть проверок из бизнес-логики и делает URL-контракт явным.

Кастомные ограничения реализуются через IRouteConstraint и регистрируются в DI:

Код ITЗагрузка примера кода…

Разбор:

  • Configure<RouteOptions>(...) настраивает поведение маршрутизации на уровне приложения.
  • ConstraintMap.Add("slug", ...) регистрирует пользовательский constraint под именем slug.
  • SlugConstraint.Match(...) проверяет формат значения маршрута регулярным выражением.
  • Если значение не проходит проверку, endpoint не выбирается и запрос идёт по другим маршрутным правилам.

Route-to-code binding

Процесс превращения URL в вызов метода включает:

  1. Сопоставление шаблона
    URL /api/products/123 сопоставляется с шаблоном api/[controller]/{id}controller = "Products", id = "123".

  2. Поиск контроллера
    По соглашению ищется класс ProductsController (суффикс Controller добавляется автоматически).

  3. Поиск метода (action)

    • Сопоставление по HTTP-методу ([HttpGet]),
    • Сопоставление по имени (если шаблон содержит [action], иначе — по соглашению — Get, Post, GetById и т.д.),
    • Сопоставление по параметрам (количество и имена должны совпадать с параметрами метода или route/query).
  4. Привязка модели (Model Binding)
    Значения параметров маршрута (id = "123") передаются в параметры метода. Если тип не совпадает (например, int id), вызывается TypeConverter или ModelBinder.

Если ни один endpoint не найден — возвращается 404. Если найдено несколько — возникает неоднозначность (ambiguous match), и приложение не запустится (ошибка на этапе регистрации).


Расширяемость маршрутизации

ASP.NET Core позволяет создавать кастомные endpoint’ы и маршруты:

  • Кастомные endpoint builders
    Например, MapHealthChecks() — это extension method, который регистрирует endpoint с делегатом проверки здоровья.

  • Кастомные маршрутизаторы (IRouter)
    Реализация IRouter даёт полный контроль над логикой сопоставления. Используется редко (например, для legacy-совместимости).

  • Dynamic endpoint registration
    Можно регистрировать endpoint’ы динамически при обработке запроса (например, для CMS с URL из БД):

app.MapDynamicControllerRoute<CustomTransformer>("/content/{**slug}");

Разбор:

  • MapDynamicControllerRoute<CustomTransformer>(...) подключает динамический роутинг.
  • {**slug} — catch-all параметр, который захватывает весь остаток URL.
  • CustomTransformer преобразует slug в конкретные route values (контроллер, action, параметры).
  • Подход удобен для CMS и систем с маршрутами, которые хранятся во внешнем источнике. где CustomTransformer реализует IDynamicEndpointTransformer.

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


6. Шаблонизация и представления

Razor Engine — от шаблона к коду

Razor — это язык шаблонов, позволяющий встраивать C#-код в HTML-подобную разметку. Его ключевая особенность — контекстно-зависимый парсинг — Razor различает HTML-контекст и код-контекст на основе символов (@, {, }, ;), что делает синтаксис лаконичным.

Но важно понимать: Razor — это не интерпретатор. Это компилятор, который на этапе сборки (или при первом запросе) преобразует .cshtml-файл в C#-класс, унаследованный от RazorPage<TModel>. Например, для Index.cshtml генерируется класс Index, содержащий метод ExecuteAsync(), в котором:

  • HTML-литералы → вызовы WriteLiteral(),
  • @model.Name → вызовы Write(Model.Name),
  • @{ ... } → встраивание C#-блока.

Этот класс компилируется в сборку (обычно в App.dll при публикации с RazorCompileOnPublish=true), что обеспечивает:

  • Типовую безопасность — ошибки в шаблоне обнаруживаются на этапе компиляции,
  • Высокую производительность — нет парсинга шаблона при каждом запросе,
  • Поддержку отладки — можно ставить точки останова в .cshtml.

В режиме разработки (Разработка) Razor поддерживает динамическую компиляцию: при изменении .cshtml файл перекомпилируется "на лету", без перезапуска приложения.


Безопасность шаблонов — auto-encoding и XSS

По умолчанию Razor автоматически экранирует весь вывод через @:

<p>@userInput</p> <!-- userInput = "<script>alert(1)</script>" -->
<!-- Результат: &lt;script&gt;alert(1)&lt;/script&gt; -->

Разбор:

  • @userInput вставляет значение переменной в HTML-шаблон Razor.
  • По умолчанию Razor экранирует потенциально опасные символы (<, >, "), поэтому скрипт не исполняется.
  • Итоговый вывод становится текстом на странице, а не активным JavaScript-кодом.

Это предотвращает XSS-атаки. Экранирование применяется к string, object, HtmlString (если не помечен как доверенный).

Для вывода недоверенного HTML используется Html.Raw():

@Html.Raw(Model.TrustedHtml) <!-- Только если HTML прошёл санитизацию! -->

Разбор:

  • Html.Raw(...) отключает автоэкранирование Razor и выводит HTML "как есть".
  • Использовать этот вызов можно только для заранее очищенного и доверенного контента.
  • При вводе пользователя без санитизации такой код открывает прямой путь к XSS-уязвимостям. Но это опасная операция — её следует применять только к данным, прошедшим строгую валидацию и очистку (например, через библиотеку HtmlSanitizer).

Композиция UI — уровни переиспользования

ASP.NET Core предоставляет иерархию механизмов для повторного использования разметки — от простых фрагментов до полноценных компонентов.

  1. Layout (макет)
    Файл _Layout.cshtml задаёт общую структуру страницы:
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<header>...</header>
<main>
@RenderBody() <!-- Содержимое конкретной страницы -->
</main>
<footer>...</footer>
@RenderSection("Scripts", required: false) <!-- Опциональные скрипты -->
</body>
</html>

Разбор:

  • Этот шаблон задаёт общий HTML-каркас для всех страниц, использующих layout.

  • @RenderBody() вставляет контент конкретной view в центральную область.

  • @RenderSection("Scripts", required: false) позволяет странице подключать свои скрипты в контролируемой точке.

  • Подход сокращает дублирование и централизует структуру интерфейса.

    Страница указывает layout через:

@{
Layout = "_Layout";
}

Разбор:

  • Блок @{ ... } содержит серверный C#-код внутри Razor.

  • Layout = "_Layout" назначает макет для текущей страницы.

  • Имя _Layout связывается с файлом макета, который оборачивает содержимое страницы.

    или глобально в _ViewStart.cshtml (см. ниже).

  1. _ViewStart.cshtml
    Специальный файл, выполняемый перед каждой View или Page. Обычно используется для задания общего Layout:
@{
Layout = "_Layout";
}

Разбор:

  • В _ViewStart.cshtml этот код применяется ко всем страницам в соответствующей папке и её подпапках.

  • Настройка задаёт единый layout по умолчанию для группы view/page.

  • При необходимости конкретная страница может переопределить layout локально.

    Располагается в папке Views/ (для MVC) или Pages/ (для Razor Pages). Иерархия: _ViewStart в подпапке переопределяет родительский.

  1. Partial Views (частичные представления)
    Фрагменты разметки без логики (_ProductCard.cshtml), включаемые в другие страницы:
<partial name="_ProductCard" model="Model.Product" />
<!-- Или через HTML-хелпер: @Html.Partial("_ProductCard", Model.Product) -->

Разбор:

  • <partial ... /> вставляет частичное представление внутрь текущего шаблона.
  • name указывает файл partial, а model передаёт ему данные.
  • Partial удобен для повторного использования небольших UI-фрагментов.
  • В отличие от View Component, partial обычно не содержит отдельной серверной логики. Подходят для простых, статичных блоков (карточки, формы). Не имеют собственного Page Model.
  1. View Components
    Это полноправные компоненты с логикой и представлением. Состоят из:

    • Класса, унаследованного от ViewComponent, с методом InvokeAsync() или Invoke(),
    • Представления Default.cshtml в папке Views/Shared/Components/ComponentName/.

    Пример:

public class PriorityListViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(int maxPriority)
{
var items = await GetItemsAsync(maxPriority);
return View(items);
}
}

Разбор:

  • ViewComponent инкапсулирует серверную логику и собственный шаблон вывода.

  • InvokeAsync(int maxPriority) принимает параметры компонента и возвращает результат рендеринга.

  • await GetItemsAsync(...) получает данные асинхронно перед генерацией HTML.

  • return View(items) передаёт модель в представление компонента.

    Вызов в шаблоне:

@await Component.InvokeAsync("PriorityList", new { maxPriority = 3 })
<!-- Или через Tag Helper: <vc:priority-list max-priority="3" /> -->

Разбор:

  • Component.InvokeAsync(...) рендерит View Component прямо из Razor-шаблона.
  • Первый аргумент — имя компонента, второй — объект параметров для метода InvokeAsync.
  • В примере передаётся maxPriority = 3, который влияет на выборку данных.
  • Вариант с <vc:...> делает тот же вызов в декларативной форме через Tag Helper. Преимущества перед Partial Views:
    — Поддержка DI (можно принимать сервисы в конструкторе),
    — Асинхронность,
    — Тестирование как обычного класса.

Tag Helpers — серверная логика в HTML

Tag Helpers — это компоненты, расширяющие HTML-теги декларативной серверной логикой. В отличие от HTML-хелперов (например, @Html.ActionLink()), они не нарушают читаемость разметки.

Примеры встроенных Tag Helper’ов:

  • <a asp-controller="Home" asp-action="Index">
    Генерирует <a href="/Home/Index">, автоматически учитывая маршруты и параметры.

  • <form asp-action="Submit" method="post"> Добавляет action="/Controller/Submit", AntiForgeryToken, управляет методом.

  • <input asp-for="Email" /> Генерирует <input name="Email" id="Email" value="@Model.Email" />, с поддержкой DataAnnotations (валидация, типы).

  • <environment include="Разработка">
    Условный рендеринг для окружений.

Создание кастомного Tag Helper’а:

  1. Класс, унаследованный от TagHelper, с атрибутом [HtmlTargetElement]:
[HtmlTargetElement("email", Attributes = "address")]
public class EmailTagHelper : TagHelper
{
public string Address { get; set; } = string.Empty;

public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", $"mailto:{Address}");
output.Content.SetContent(Address);
}
}

Разбор:

  • [HtmlTargetElement("email", Attributes = "address")] связывает helper с тегом <email ...>.
  • Наследование от TagHelper позволяет программно преобразовать пользовательский тег в стандартный HTML.
  • В Process(...) задаются имя итогового тега, атрибуты и содержимое.
  • В примере формируется ссылка mailto: на основе значения свойства Address.
  1. Регистрация в _ViewImports.cshtml:
@addTagHelper *, MyApp

Разбор:

  • @addTagHelper подключает Tag Helper-ы из указанной сборки.
  • * означает, что импортируются все helper-классы в сборке MyApp.
  • После этой строки кастомные теги становятся доступны в Razor-шаблонах.
  1. Использование:
<email address="support@example.com" />
<!-- Результат: <a href="mailto:support@example.com">support@example.com</a> -->

Разбор:

  • Тег <email ... /> — декларативный синтаксис использования созданного helper-а.
  • Атрибут address попадает в свойство Address внутри EmailTagHelper.
  • На этапе рендеринга Razor заменит этот тег на обычный <a href="mailto:...">...</a>.

Преимущества Tag Helpers:

  • Сохраняют HTML-подобный синтаксис,
  • Поддерживаются инструментами (IntelliSense, валидация в IDE),
  • Изолированы — не влияют на другие теги.

Кэширование шаблонов

Для повышения производительности ASP.NET Core кэширует:

  • Скомпилированные типы страниц (в памяти),
  • Результаты рендеринга (если используется [ResponseCache] или IMemoryCache).

Кэш компиляции сбрасывается при изменении .cshtml (в режиме разработки) или при перезапуске приложения. В production-сборках шаблоны компилируются статически, и кэш не требуется.

Для динамического контента (например, новостная лента) применяется фрагментное кэширование через Tag Helper <cache>:

<cache expires-after="@TimeSpan.FromMinutes(10)">
<partial name="_NewsFeed" model="Model.News" />
</cache>

Разбор:

  • <cache ...> кэширует результат рендеринга вложенного HTML-фрагмента.
  • expires-after задаёт TTL кэша в данном случае на 10 минут.
  • Пока кэш не истёк, сервер возвращает готовый фрагмент без повторного рендеринга partial.
  • Это снижает нагрузку на сервер для часто запрашиваемых и редко меняющихся блоков. Кэш учитывает параметры (например, vary-by-user), чтобы разные пользователи видели своё содержимое.

7. Конфигурация и управление параметрами

Единая модель IConfiguration

Корень всей конфигурации — интерфейс Microsoft.Extensions.Configuration.IConfiguration. Он предоставляет:

  • Иерархический доступ через ключи с разделителем : (например, "Database:ConnectionString"),
  • Единообразное API для чтения значений (GetSection, GetValue<T>),
  • Ленивую загрузку — значения считываются из источника только при первом обращении.

IConfiguration строится как стек поставщиков (providers). Каждый поставщик — это реализация IConfigurationProvider, которая загружает данные из конкретного источника. При чтении значения фреймворк проходит по стеку снизу вверх и возвращает первое найденное значение — это позволяет переопределять настройки более приоритетными источниками.


Поставщики конфигурации и их приоритеты

Стандартные поставщики (в порядке регистрации, т.е. увеличения приоритета):

  1. Файлы (appsettings.json, appsettings.{Environment}.json)
    Основной источник. Поддерживает вложенность через JSON-объекты:
{
"Database": {
"ConnectionString": "Server=...",
"Timeout": 30
}
}

Разбор:

  • Это пример секции Database в appsettings.json.
  • Вложенная структура естественно отображается в типизированный класс опций.
  • ConnectionString и Timeout потом можно получать через IOptions<DatabaseOptions>. Файл окружения (например, appsettings.Production.json) переопределяет значения из базового файла.
  1. Переменные среды
    Ключи преобразуются: __ заменяется на : (например, Database__ConnectionString). Это позволяет задавать настройки в Docker, Kubernetes, Azure App Settings без изменения кода.

  2. Аргументы командной строки
    Формат: --key=value или /key value. Используется для переопределения в CI/CD или при локальном запуске.

  3. Пользовательские секреты (User Secrets)
    Только в Разработка. Хранит чувствительные данные (пароли, ключи) в зашифрованном файле вне репозитория (%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json). Активируется через AddUserSecrets<Program>().

  4. Azure Key Vault, Consul, etcd
    Через сторонние пакеты (Azure.Extensions.AspNetCore.Configuration.Secrets). Для production-секретов.

Порядок регистрации в Program.cs определяет приоритет:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddCommandLine(args);

Разбор:

  • CreateBuilder(args) создаёт хост и начальную конфигурацию приложения.
  • AddJsonFile(...) подключает файлы настроек по окружениям.
  • AddEnvironmentVariables() добавляет слой конфигурации из переменных среды.
  • AddCommandLine(args) даёт возможность финального переопределения параметров через аргументы запуска.
  • Приоритет определяется порядком: более поздние providers перекрывают более ранние. Здесь CommandLine имеет наивысший приоритет — его значения переопределят всё остальное.

Options pattern — типизированный доступ к конфигурации

Прямое использование IConfiguration["key"] не рекомендуется: это нарушает типовую безопасность и усложняет тестирование. Вместо этого применяется Options pattern — привязка конфигурации к POCO-классам.

  1. Определение класса настроек:
public class DatabaseOptions
{
public string ConnectionString { get; set; } = string.Empty;
public int Timeout { get; set; } = 30;
}

Разбор:

  • DatabaseOptions описывает структуру параметров базы данных в типобезопасном виде.
  • Значения по умолчанию снижают риск null и неинициализированных полей.
  • Такой класс используется как контракт между конфигурацией и сервисами.
  1. Регистрация в DI:
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database")
);

Разбор:

  • Configure<DatabaseOptions>(...) регистрирует привязку секции конфигурации к классу опций.
  • GetSection("Database") выбирает источник данных внутри общего IConfiguration.
  • После регистрации опции доступны через IOptions<DatabaseOptions>, IOptionsSnapshot и IOptionsMonitor.
  1. Инъекция в компоненты:
public class MyService
{
private readonly DatabaseOptions _options;

public MyService(IOptions<DatabaseOptions> options)
{
_options = options.Value; // получение значения
}
}

Разбор:

  • В конструктор сервиса внедряется IOptions<DatabaseOptions>.
  • options.Value возвращает типизированные настройки, готовые к использованию в коде.
  • Поле _options хранит значения конфигурации и делает зависимость сервиса явной.

Три интерфейса для работы с опциями:

  • IOptions<T> — синглтон-значение, загружаемое при старте приложения. Не реагирует на изменения конфигурации.

  • IOptionsSnapshot<T> — создаётся на каждый запрос (scoped). Подходит для сценариев, где настройки могут меняться между запросами (например, multi-tenant).

  • IOptionsMonitor<T> — позволяет подписаться на изменения конфигурации:

_monitor.OnChange(options =>
{
logger.LogInformation("Database timeout changed to {Timeout}", options.Timeout);
});

Разбор:

  • OnChange(...) подписывает код на изменения конфигурации во время работы приложения.
  • Аргумент options содержит уже обновлённые значения DatabaseOptions.
  • В примере изменение Timeout сразу фиксируется в логах для диагностики. Требует поддержки reload’а от поставщика (например, AddJsonFile(..., reloadOnChange: true)).

Валидация конфигурации

Options pattern поддерживает валидацию через Data Annotations и кастомные правила:

  1. Атрибуты валидации:
public class DatabaseOptions
{
[Required]
public string ConnectionString { get; set; } = string.Empty;

[Range(1, 300)]
public int Timeout { get; set; } = 30;
}

Разбор:

  • Атрибут [Required] делает поле обязательным при валидации настроек.
  • [Range(1, 300)] ограничивает допустимый диапазон значения Timeout.
  • Такие аннотации переводят ошибки конфигурации в ранний и понятный fail-fast.
  1. Регистрация с валидацией:
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration("Database")
.ValidateDataAnnotations()
.Validate(options => options.Timeout > 0, "Timeout must be positive.");

Разбор:

  • AddOptions<DatabaseOptions>() начинает цепочку настройки и валидации опций.
  • BindConfiguration("Database") связывает класс с одноимённой секцией в конфигурации.
  • ValidateDataAnnotations() включает проверки атрибутов (Required, Range и т.д.).
  • Validate(...) добавляет произвольное бизнес-правило для опций.
  1. Проверка при старте:
var app = builder.Build();
app.ValidateOptions(); // выбросит исключение при невалидных настройках

Разбор:

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

Это гарантирует, что приложение не запустится с некорректной конфигурацией.


Создание кастомных поставщиков

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

  1. Реализация IConfigurationProvider:

Код ITЗагрузка примера кода…

Разбор:

  • Класс наследуется от ConfigurationProvider и реализует собственный источник настроек.
  • В Load() выполняется чтение ключей и значений из базы данных.
  • Результат помещается в data, которую использует IConfiguration.
  • Такой provider позволяет централизовать runtime-конфигурацию вне файлов.
  1. Реализация IConfigurationSource:

Код ITЗагрузка примера кода…

Разбор:

  • IConfigurationSource отвечает за создание конкретного provider-а.
  • Класс хранит параметры подключения и передаёт их в DatabaseConfigurationProvider.
  • Метод Build(...) вызывается конфигурационным пайплайном при старте приложения.
  1. Метод расширения для удобства:
public static class ConfigurationExtensions
{
public static IConfigurationBuilder AddDatabaseConfiguration(
this IConfigurationBuilder builder, string connectionString)
{
return builder.Add(new DatabaseConfigurationSource(connectionString));
}
}

Разбор:

  • Это extension method для удобного подключения кастомного provider-а.
  • Метод расширяет IConfigurationBuilder, сохраняя привычный fluent-синтаксис.
  • Вызов builder.Add(...) добавляет DatabaseConfigurationSource в стек конфигурации.
  1. Регистрация:
builder.Configuration.AddDatabaseConfiguration(
builder.Configuration["DbConfig:ConnectionString"]
);

Разбор:

  • Код подключает кастомный источник конфигурации к общему стеку приложения.
  • Строка подключения берётся из ранее зарегистрированных источников (DbConfig:ConnectionString).
  • После этого значения из базы могут участвовать в переопределении настроек.

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


Иерархия конфигурации и привязка

Конфигурация поддерживает сложные структуры:

  • Массивы:
"AllowedHosts": [ "localhost", "example.com" ]

Разбор:

  • JSON-массив задаёт несколько разрешённых хостов в одной секции.

  • Конфигурационный binder преобразует такой блок в List<string>.

  • Подход удобен для whitelists и списков окружений. Привязка к List<string> AllowedHosts.

  • Вложенные объекты:

"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}

Разбор:

  • Вложенный объект описывает иерархию настроек логирования.

  • Ключи уровня LogLevel позволяют задавать разные уровни для разных категорий.

  • Такая структура хорошо маппится в вложенные классы или словари. Привязка к LoggingOptions с вложенным Dictionary<string, string> LogLevel.

  • Секции как отдельные объекты:

builder.Services.Configure<SmtpOptions>(
builder.Configuration.GetSection("Email:Smtp")
);

Разбор:

  • Пример показывает привязку не корневой, а глубоко вложенной секции Email:Smtp.
  • Configure<SmtpOptions> делает настройки SMTP доступными через DI.
  • Это позволяет изолировать почтовые параметры в отдельный объект опций.

Привязка выполняется через Microsoft.Extensions.Configuration.Binder, который использует рефлексию и поддерживает:

  • Простые типы (int, bool, string),
  • Коллекции (List<T>, Dictionary<K,V>),
  • Комплексные объекты (рекурсивно),
  • Кастомные конвертеры (TypeConverter).

8. Внедрение зависимостей (DI)

Встроенная реализация — возможности и ограничения

ASP.NET Core поставляется с минимальным, но достаточным DI-контейнером. Он поддерживает:

  • Регистрацию сервисов с указанием времени жизни,
  • Разрешение зависимостей через конструктор (constructor injection),
  • Интеграцию с фреймворком (контроллеры, middleware, hosted services автоматически разрешаются из DI),
  • Проверку циклических зависимостей на этапе сборки (при использовании ValidateScopes).

Ограничения встроенного контейнера (по сравнению с Autofac, Lamar, Ninject):

  • Нет поддержки регистрации по соглашению (convention-based registration),
  • Нет декораторов, интерсепторов, property injection,
  • Нет поддержки IEnumerable<T> с разным временем жизни (все элементы будут иметь время жизни самого IEnumerable),
  • Нет поддержки keyed/named dependencies.

Эти ограничения намеренны: они поощряют простые, тестируемые архитектуры. Для сложных сценариев можно подключить сторонний контейнер — фреймворк предоставляет стандартный интерфейс IServiceProviderFactory<T>.


Время жизни сервисов

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

  1. Singleton

    • Один экземпляр на всё приложение.
    • Когда использовать — логгеры, утилиты без состояния, кэши-обёртки, глобальные конфигурации.
    • Опасности:
      — Хранение состояния (например, List<T> _items) приведёт к гонкам данных,
      — Зависимость от scoped-сервисов (например, DbContext) вызовет InvalidOperationException при попытке разрешения.
  2. Scoped

    • Один экземпляр на HTTP-запрос (или на сессию в фоновой задаче).
    • Когда использовать:
      DbContext (EF Core требует одного контекста на запрос для отслеживания изменений),
      — Сервисы с состоянием, привязанным к запросу (например, ICurrentUserService),
      — Unit of Work, репозитории в рамках одного запроса.
    • Опасности:
      — Использование в singleton’ах (см. выше),
      — Передача scoped-сервиса в фоновый поток без создания scope’а.
  3. Transient

    • Новый экземпляр при каждом разрешении.
    • Когда использовать:
      — Простые, stateless-сервисы (валидаторы, мапперы, фабрики),
      — Сервисы, которые создают scoped-зависимости внутри себя (например, IServiceScopeFactory.CreateScope()).
    • Опасности:
      — Создание "тяжёлых" объектов (например, подключения к БД) — приведёт к утечкам ресурсов,
      — Неявное создание множества экземпляров при вложенных разрешениях.

Важное уточнение: время жизни определяет жизненный цикл экземпляра, а не время жизни ссылки. DI-контейнер управляет временем жизни только для объектов, созданных им самим. Если вы создаёте экземпляр вручную (new MyService()), контейнер не отслеживает его.


Практический шаблон регистрации DI

builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ISystemClock, SystemClock>();
builder.Services.AddTransient<IEmailTemplateRenderer, EmailTemplateRenderer>();

Разбор:

  • AddScoped создаёт один экземпляр сервиса на HTTP-запрос.
  • AddSingleton создаёт единственный экземпляр на всё время жизни приложения.
  • AddTransient создаёт новый экземпляр при каждом запросе зависимости.
  • Пример показывает типичную базовую матрицу lifetimes для веб-приложения.

Такой стартовый шаблон покрывает типичные нужды API и веб-приложений. Дальше lifetimes уточняют по профилированию и нагрузочным тестам.


DI в различных контекстах

  1. В контроллерах и Razor Pages
    Зависимости инъектируются через конструктор. Фреймворк автоматически разрешает их из DI:
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;

public ProductsController(IProductService productService)
{
_productService = productService;
}
}

Разбор:

  • Контроллер получает IProductService через constructor injection.
  • Поле _productService хранит зависимость для дальнейшего использования в action-методах.
  • Такой стиль делает зависимости явными и упрощает unit-тестирование контроллера.
  1. В middleware
    Middleware создаётся один раз при старте приложения (не на каждый запрос!), поэтому зависимости нельзя инъектировать в конструктор — они будут "заморожены" как singleton’ы.
    Правильный способ — получать scoped-сервисы через context.RequestServices:

Код ITЗагрузка примера кода…

Разбор:

  • Middleware хранит только _next в конструкторе и переиспользуется между запросами.
  • В InvokeAsync(...) scoped-зависимости безопасно запрашиваются через context.RequestServices.
  • await _next(context) передаёт управление следующему звену pipeline.
  • Такой подход избегает ошибки "singleton зависит от scoped".
  1. В фоновых задачах (IHostedService)
    IHostedService создаётся как singleton. Для выполнения scoped-операций нужно вручную создавать scope:

Код ITЗагрузка примера кода…

Разбор:

  • IHostedService запускается как singleton-фоновый сервис.
  • CreateScope() создаёт отдельный DI-scope для scoped-сервисов внутри фоновой операции.
  • Через scope.ServiceProvider безопасно получается AppDbContext.
  • Это правильный паттерн для фоновых задач, работающих с БД.
  1. В SignalR hubs
    Хабы создаются на каждое соединение (de facto scoped). Зависимости инъектируются через конструктор, как в контроллерах.

Интеграция сторонних DI-контейнеров

Для подключения Autofac, Lamar и др. используется IServiceProviderFactory<T>:

var builder = WebApplication.CreateBuilder(args);

// Отключаем валидацию в built-in DI (она не нужна при использовании стороннего контейнера)
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Регистрация в Autofac
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
containerBuilder.RegisterModule<MyAutofacModule>();
});

Разбор:

  • UseServiceProviderFactory(...) переключает приложение на внешний DI-контейнер Autofac.
  • ConfigureContainer<ContainerBuilder>(...) добавляет контейнер-специфичную регистрацию модулей.
  • Такой подход сохраняет совместимость со стандартными builder.Services и расширяет возможности DI.

Контейнер создаётся после регистрации всех сервисов через builder.Services, поэтому:

  • Сначала регистрируем через IServiceCollection (стандартный путь),
  • Затем донастраиваем в ConfigureContainer (специфичные фичи контейнера).

Это обеспечивает совместимость с библиотеками, которые регистрируют зависимости через IServiceCollection (например, EF Core, Identity).


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

  • Избегайте Service Locator (IServiceProvider.GetService<T>() в бизнес-логике). Это скрывает зависимости и усложняет тестирование. Используйте только в middleware и hosted services, где constructor injection невозможен.

  • Не смешивайте времена жизни без необходимости. Если сервис A (scoped) зависит от B (singleton), это допустимо. Но если B (singleton) зависит от A (scoped) — будет исключение.

  • Тестируйте разрешение зависимостей. Используйте Host.CreateDefaultBuilder().Build().Services в интеграционных тестах, чтобы убедиться, что все зависимости разрешаются.

  • Регистрируйте интерфейсы, а не конкретные классы — это упрощает подмену реализаций (например, для моков в тестах).


9. Работа с данными в веб-приложении

Слои данных и их границы

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

  1. Transport Layer (HTTP)
    — Входящие данные — query string, route parameters, form fields, JSON/XML тело запроса,
    — Исходящие данные — JSON, XML, HTML, файлы.
    Ответственность: сериализация/десериализация, валидация формата.

  2. Application Layer (контроллеры, страницы)
    — Принимает транспортные объекты (DTO),
    — Оркестрирует вызовы сервисов,
    — Преобразует результаты в транспортные объекты.
    Ответственность: координация, не бизнес-логика.

  3. Domain Layer (сервисы, агрегаты)
    — Содержит бизнес-правила, валидацию предметной области, инварианты,
    — Оперирует доменными моделями (rich domain objects),
    — Не знает о HTTP, DTO, ORM.
    Ответственность: "почему?" — почему операция разрешена/запрещена.

  4. Infrastructure Layer (репозитории, ORM, внешние API)
    — Реализует доступ к данным,
    — Преобразует доменные модели ↔ DTO хранилища,
    — Обеспечивает транзакционность.
    Ответственность: "как?" — как сохранить/загрузить данные.

Ключевые типы объектов и их назначение:

  • Domain Model — классы предметной области с поведением (например, Order.AddLineItem(Product, int quantity)), инкапсулирующие бизнес-правила. Не должны содержать атрибутов сериализации или ORM.

  • DTO (Data Transfer Object) — плоские, неизменяемые объекты для передачи данных между слоями (например, CreateOrderRequest, OrderResponse). Используются в контроллерах и при вызове внешних сервисов.

  • ViewModel — DTO, специфичные для представления (например, ProductDetailsPageModel), содержащие данные, необходимые именно для отрисовки страницы (включая выпадающие списки, флаги видимости и т.д.).

  • Entity (ORM Entity) — классы, отображаемые на таблицы БД (например, OrderEntity). Содержат атрибуты ORM ([Key], [Column]) и могут отличаться от Domain Model (например, содержать технические поля вроде RowVersion).

Разделение этих типов предотвращает утечку абстракций: например, атрибуты валидации [Required] для UI не должны влиять на бизнес-правила в домене.


Model Binding — от HTTP к объекту

Model Binding — механизм автоматического заполнения параметров действия (action method) из источников HTTP-запроса. Он работает до вызова метода контроллера и является частью конвейера MVC.

Источники данных и приоритеты:

  1. Route Values ({id} в шаблоне) — высший приоритет для параметров с совпадающими именами,
  2. Query String (?page=2&size=10),
  3. Form Data (application/x-www-form-urlencoded, multipart/form-data),
  4. Request Body (application/json, application/xml) — только для одного параметра (обычно сложного типа),
  5. Files — через IFormFile.

Управление привязкой через атрибуты:

  • [FromRoute], [FromQuery], [FromForm], [FromBody] — явное указание источника,
  • [BindRequired] — параметр обязателен, иначе ModelState будет invalid,
  • [BindNever] — исключить параметр из привязки (защита от overposting),
  • [ModelBinder(typeof(MyCustomBinder))] — кастомный биндер.

Кастомный Model Binder реализует IModelBinder и регистрируется глобально или через атрибут:

public class SlugBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue("slug").FirstValue;
if (value != null && SlugHelper.IsValid(value))
{
bindingContext.Result = ModelBindingResult.Success(new Slug(value));
}
return Task.CompletedTask;
}
}

Разбор:

  • IModelBinder позволяет определить собственную логику преобразования входных данных в доменный тип.
  • ValueProvider.GetValue("slug") извлекает значение параметра из доступных источников запроса.
  • При успешной проверке bindingContext.Result заполняется объектом Slug.
  • Если валидация не проходит, биндер оставляет результат пустым, и модель считается непривязанной.

Валидация — ModelState и политики

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

  1. Валидация формата (Model State Validation)
    — Проверка типов ("abc"int = ошибка),
    — Проверка атрибутов DataAnnotations ([Required], [StringLength]),
    — Результат сохраняется в ModelState.IsValid.

  2. Бизнес-валидация (Domain Validation)
    — Проверка инвариантов в доменных сервисах (OrderService.CreateOrder() может вернуть ValidationResult),
    — Не зависит от HTTP-контекста.

Глобальные фильтры валидации:
Атрибут [ApiController] автоматически возвращает 400 при !ModelState.IsValid. Это можно настроить через:

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.ToDictionary(
e => e.Key,
e => e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
);
return new BadRequestObjectResult(errors);
};
});

Разбор:

  • Конфигурация ApiBehaviorOptions переопределяет стандартный ответ при невалидном ModelState.
  • Код собирает ошибки в словарь поле -> массив сообщений.
  • BadRequestObjectResult(errors) возвращает клиенту структурированный 400 с деталями.
  • Это делает формат ошибок единым и удобным для фронтенда.

Для сложных сценариев (например, условная валидация) используется IValidatableObject:

Код ITЗагрузка примера кода…

Разбор:

  • Реализация IValidatableObject добавляет объектную (межполевая) валидацию.
  • Метод Validate(...) вызывается после базовых DataAnnotations-проверок.
  • Условие сравнивает Password и ConfirmPassword как бизнес-правило формы.
  • yield return new ValidationResult(...) добавляет ошибку, связанную с конкретным полем.

ORM-стек

  1. Entity Framework Core (EF Core) — полноценный ORM с поддержкой:
    — Отслеживания изменений (change tracking),
    — Ленивой загрузки (lazy loading, опционально),
    — Миграций кода первой (code-first migrations),
    — Поддержки отношений (one-to-many, many-to-many),
    — Глобальных фильтров (soft delete),
    — Owned Types (вложенные объекты как часть агрегата).
    Когда использовать: приложения с богатой доменной моделью, где важна продуктивность разработки и поддержка сложных сценариев.

  2. Dapper — микро-ORM, выполняющий только маппинг результатов SQL-запросов в объекты.
    — Высокая производительность (близка к ADO.NET),
    — Полный контроль над SQL,
    — Нет отслеживания изменений — нужно писать UPDATE вручную.
    Когда использовать — high-load read-операции, legacy-БД с неудобной схемой, микросервисы с простыми CRUD-операциями.

  3. ADO.NET — низкоуровневый доступ к БД через SqlConnection, SqlCommand, SqlDataReader.
    — Максимальная производительность и контроль,
    — Требует ручной обработки соединений, параметров, маппинга.
    Когда использовать: критически важные операции (bulk insert; теория — Пакетная работа с данными), интеграция с нестандартными СУБД, кастомные провайдеры.

Паттерны доступа к данным:

  • Repository Pattern
    Абстрагирует доступ к данным за интерфейсом (IProductRepository), скрывая детали ORM. Позволяет легко подменять реализации (например, для тестов).
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> ListAsync();
Task AddAsync(Product product);
}

Разбор:

  • Интерфейс репозитория скрывает детали хранения данных от бизнес-слоя.

  • Методы разделяют операции чтения (GetByIdAsync, ListAsync) и записи (AddAsync).

  • Такой контракт упрощает замену реализации (EF Core, Dapper, mock в тестах).

  • Unit of Work
    Обеспечивает атомарность операций над несколькими репозиториями через общую транзакцию. В EF Core это DbContext — он сам является Unit of Work:

using var transaction = await _context.Database.BeginTransactionAsync();
try
{
await _productRepo.AddAsync(product);
await _orderRepo.AddAsync(order);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}

Разбор:

  • BeginTransactionAsync() явно открывает транзакцию для группы операций.

  • В try выполняются несколько репозиторных действий и единый SaveChangesAsync().

  • CommitAsync() фиксирует изменения только после успешного завершения всех шагов.

  • В catch выполняется RollbackAsync(), чтобы не оставить систему в частично обновлённом состоянии.

  • CQRS (Command Query Responsibility Segregation)
    Разделение операций на:
    Commands (изменение состояния: CreateOrderCommand),
    Queries (чтение: GetOrderQuery).
    Позволяет оптимизировать каждый путь отдельно (например, использовать разные БД для записи и чтения).


Безопасность при работе с данными

  • Параметризованные запросы — обязательны для предотвращения SQL-инъекций. EF Core и Dapper делают это автоматически при использовании ExecuteAsync(sql, param).
  • Ограничение overposting — использование [BindNever], BindProperty в Razor Pages, DTO вместо domain models в контроллерах.
  • Пагинация — никогда не возвращать IQueryable<T> из сервисов; всегда применять Skip/Take на уровне репозитория.
  • Фильтрация на уровне БД — избегать .ToList().Where(...) для больших таблиц.

Связанное углубление:


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Веб-разработкаВеб-разработка и API на C#, Документация и практика ASP.NET (Microsoft Learn), C# — о разделе, Приложение с S3, PostgreSQL и ASP.NET Core Web API, ASP.NET - веб-платформа Microsoft, Python — о разделе.