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 Core | HTTP, MapGet, Swagger, контроллер |
| 2 | Minimal API и OpenAPI | Группы маршрутов, типы ответов |
| 3 | Razor Pages | HTML-формы с сервера |
| 4 | EF Core или ADO.NET / Dapper | Данные в БД |
| 5 | Identity — JWT и cookie | API, MVC, роли |
| 6 | Тесты — юнит и интеграция | Moq для MVC, WebApplicationFactory для API |
| 7 | Blazor / MAUI | UI в браузере или в сторе |
| 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 поддерживает три основные модели хостинга, различающиеся по способу запуска и управлению процессом.
-
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).
-
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-порты.
- На Windows используется
-
Интеграция с 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-запроса
Как выбрать модель развёртывания в реальном проекте
| Сценарий | Базовый выбор | Что учесть |
|---|---|---|
| Внутренний сервис на Linux | Standalone + reverse proxy | Логи, health checks, systemd |
| Корпоративная Windows-инфраструктура | IIS In-Process | Централизованное администрирование IIS |
| Контейнерная платформа (Kubernetes) | Standalone/SCD в контейнере | Stateless-дизайн и внешняя конфигурация |
| Изолированная среда без общего runtime | SCD | Больше размер артефакта |
Такой выбор обычно фиксируют в архитектурном решении проекта и пересматривают при росте нагрузки.
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
Порядок критичен. Вот рекомендуемая последовательность (с обоснованием):
-
UseExceptionHandler/UseDeveloperExceptionPage
Самый первый — чтобы перехватывать исключения на всех уровнях. -
UseHttpsRedirection
Раннее перенаправление с HTTP на HTTPS — до любой бизнес-логики. -
UseStaticFiles
Обслуживаниеwwwroot— если запрос совпадает с файлом, pipeline завершается здесь (этоRunвнутри). -
UseRouting
Не обрабатывает запрос, а только определяет endpoint. После него становятся доступны данные маршрутизации (context.GetEndpoint()), но endpoint ещё не вызван. -
UseAuthentication
Определяетcontext.User— должен быть до авторизации и бизнес-логики. -
UseAuthorization
Проверяет права на основеUserи политик — после аутентификации, до вызова endpoint. -
UseSession
ТребуетUserдля привязки сессии — после авторизации. -
UseEndpoints/MapRazorPages/MapControllers
Вызывает endpoint (контроллер, страницу и т.д.). Это терминальный middleware для основного потока. -
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 на группу API | MapGroup("/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 перед операцией "купить облигации":
- права клиента на операцию;
- регуляторные ограничения;
- торговые часы;
- региональные ограничения;
- feature flag продукта.
Запихивать все if в handler неудобно: handler должен описывать сценарий ("создать заявку"), а не каталог регуляций.
| Принцип | Смысл |
|---|---|
| Одна policy — одна проверка | ITradingHoursPolicy, IRegionPolicy — отдельные классы. |
| Порядок явный | Регистрация цепочки в DI или композиция в Application. |
| Остановка на первом отказе | Не вызывать handler, если policy вернула failure. |
| Переиспользование | Те же policies в Minimal API, MVC и фоновых job. |
Где реализовать в .NET:
| Подход | Когда уместен |
|---|---|
Ручная цепочка IPolicy<TContext> + runner | Явный контроль, мало зависимостей. |
MediatR IPipelineBehavior | Clean 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/Razor —
ActionExecutingContext,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:
- Middleware
UseRouting()сопоставляет URL с шаблоном маршрута (например,{controller=Home}/{action=Index}/{id?}). - Middleware
UseEndpoints()создаёт экземпляр контроллера через DI. - Model Binding заполняет параметры действия (action method) из источников:
[FromRoute]— из сегментов URL (/product/123→id = 123),[FromQuery]— из строки запроса (?page=2),[FromBody]— из тела (JSON/XML),[FromForm]— из multipart/form-data.
- Выполняются фильтры (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).
- Authorization filters —
- Вызывается метод контроллера (action). Он может:
- вернуть
View()с Model (рендеринг страницы), - вернуть
Json(),Ok(),CreatedAtAction()(Web API-стиль), - перенаправить (
RedirectToAction()).
- вернуть
- Если возвращён
ViewResult, запускается View Engine (Razor), который компилирует.cshtmlв C#-код, выполняет его и генерирует HTML. - Выполняются 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:
| Критерий | MVC | Razor Pages |
|---|---|---|
| Единица разработки | Контроллер + View | Page Model + Page |
| Маршрутизация | Глобальные шаблоны или атрибуты на контроллере | Автоматическая по пути файла (например, /Pages/Admin/Users/Index.cshtml → /Admin/Users/Index) |
| Состояние | Нет встроенного механизма | ModelState, TempData, ViewData работают так же, но проще управлять в рамках одной страницы |
| Сложность | Выше (разделение на 3 части) | Ниже (всё в одном месте) |
| Тестирование | Контроллеры легко тестируются изолированно | Page Model тестируется как обычный класс, но с зависимостью от PageContext |
Жизненный цикл вызова в Razor Pages:
UseRouting()сопоставляет URL с файлом страницы (по соглашению).UseEndpoints()создаёт экземпляр Page Model через DI.- Выполняются Page Filters (аналог Action Filters).
- Вызывается метод-обработчик (
OnGet(),OnPost()и т.д.).- Привязка модели происходит автоматически для свойств с
[BindProperty](по умолчанию — только для POST). - Можно использовать
[BindProperty(SupportsGet = true)]для GET-параметров.
- Привязка модели происходит автоматически для свойств с
- Если метод возвращает
Page(), запускается Razor Engine. - Выполняются 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:
- 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 для навигации.
- 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 может понадобиться. Два режима хостинга:
-
Blazor Server
- UI-логика (обработчики событий, компоненты) выполняется на сервере,
- Взаимодействие с браузером — через SignalR-соединение (WebSockets или long polling),
- Состояние компонентов хранится на сервере (в памяти),
- Преимущества: высокая производительность сервера, доступ к полному .NET API,
- Недостатки — задержки при высокой latency, масштабирование требует sticky sessions, уязвимость к потере соединения.
-
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 разделяется на два этапа:
- Регистрация endpoint’ов — происходит при запуске приложения (в
Program.cs).
Например:
endpoints.MapControllers(); // регистрирует все ApiController’ы
endpoints.MapRazorPages(); // регистрирует все Razor Pages
endpoints.MapGet("/ping", () => "OK"); // регистрирует делегат
Разбор:
MapControllers()включает endpoint-ы всех контроллеров Web API и MVC.MapRazorPages()подключает страницы Razor в общую таблицу маршрутов.MapGet("/ping", ...)добавляет минимальный endpoint без контроллера.- Регистрация выполняется на старте, а затем используется на каждом запросе во время роутинга.
- Сопоставление запроса с endpoint’ом — происходит на каждом запросе в middleware
UseRouting().
Оно определяет, какой endpoint соответствует запросу, и сохраняет его вHttpContext.GetEndpoint().
Только после этого middleware UseEndpoints() (или Map*) вызывает делегат endpoint’а.
Такое разделение позволяет middleware, зарегистрированным между UseRouting() и UseEndpoints(), получать доступ к метаданным endpoint’а (например, проверить, требует ли он авторизации), не запуская его логику.
Соглашения и атрибуты
ASP.NET Core поддерживает два способа определения маршрутов:
- Маршрутизация на основе соглашений
Глобальные шаблоны, задаваемые при регистрации 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).
- Маршрутизация на основе атрибутов
Маршруты задаются непосредственно на контроллерах и методах:
[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)— строковые правила,bool—true/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 в вызов метода включает:
-
Сопоставление шаблона
URL/api/products/123сопоставляется с шаблономapi/[controller]/{id}→controller = "Products",id = "123". -
Поиск контроллера
По соглашению ищется классProductsController(суффиксControllerдобавляется автоматически). -
Поиск метода (action)
- Сопоставление по HTTP-методу (
[HttpGet]), - Сопоставление по имени (если шаблон содержит
[action], иначе — по соглашению —Get,Post,GetByIdи т.д.), - Сопоставление по параметрам (количество и имена должны совпадать с параметрами метода или route/query).
- Сопоставление по HTTP-методу (
-
Привязка модели (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>" -->
<!-- Результат: <script>alert(1)</script> -->
Разбор:
@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 предоставляет иерархию механизмов для повторного использования разметки — от простых фрагментов до полноценных компонентов.
- 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(см. ниже).
- _ViewStart.cshtml
Специальный файл, выполняемый перед каждой View или Page. Обычно используется для задания общегоLayout:
@{
Layout = "_Layout";
}
Разбор:
-
В
_ViewStart.cshtmlэтот код применяется ко всем страницам в соответствующей папке и её подпапках. -
Настройка задаёт единый layout по умолчанию для группы view/page.
-
При необходимости конкретная страница может переопределить layout локально.
Располагается в папке
Views/(для MVC) илиPages/(для Razor Pages). Иерархия:_ViewStartв подпапке переопределяет родительский.
- 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.
-
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’а:
- Класс, унаследованный от
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.
- Регистрация в
_ViewImports.cshtml:
@addTagHelper *, MyApp
Разбор:
@addTagHelperподключает Tag Helper-ы из указанной сборки.*означает, что импортируются все helper-классы в сборкеMyApp.- После этой строки кастомные теги становятся доступны в Razor-шаблонах.
- Использование:
<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, которая загружает данные из конкретного источника. При чтении значения фреймворк проходит по стеку снизу вверх и возвращает первое найденное значение — это позволяет переопределять настройки более приоритетными источниками.
Поставщики конфигурации и их приоритеты
Стандартные поставщики (в порядке регистрации, т.е. увеличения приоритета):
- Файлы (
appsettings.json,appsettings.{Environment}.json)
Основной источник. Поддерживает вложенность через JSON-объекты:
{
"Database": {
"ConnectionString": "Server=...",
"Timeout": 30
}
}
Разбор:
- Это пример секции
Databaseвappsettings.json. - Вложенная структура естественно отображается в типизированный класс опций.
ConnectionStringиTimeoutпотом можно получать черезIOptions<DatabaseOptions>. Файл окружения (например,appsettings.Production.json) переопределяет значения из базового файла.
-
Переменные среды
Ключи преобразуются:__заменяется на:(например,Database__ConnectionString). Это позволяет задавать настройки в Docker, Kubernetes, Azure App Settings без изменения кода. -
Аргументы командной строки
Формат:--key=valueили/key value. Используется для переопределения в CI/CD или при локальном запуске. -
Пользовательские секреты (User Secrets)
Только вРазработка. Хранит чувствительные данные (пароли, ключи) в зашифрованном файле вне репозитория (%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json). Активируется черезAddUserSecrets<Program>(). -
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-классам.
- Определение класса настроек:
public class DatabaseOptions
{
public string ConnectionString { get; set; } = string.Empty;
public int Timeout { get; set; } = 30;
}
Разбор:
DatabaseOptionsописывает структуру параметров базы данных в типобезопасном виде.- Значения по умолчанию снижают риск
nullи неинициализированных полей. - Такой класс используется как контракт между конфигурацией и сервисами.
- Регистрация в DI:
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database")
);
Разбор:
Configure<DatabaseOptions>(...)регистрирует привязку секции конфигурации к классу опций.GetSection("Database")выбирает источник данных внутри общегоIConfiguration.- После регистрации опции доступны через
IOptions<DatabaseOptions>,IOptionsSnapshotиIOptionsMonitor.
- Инъекция в компоненты:
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 и кастомные правила:
- Атрибуты валидации:
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.
- Регистрация с валидацией:
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration("Database")
.ValidateDataAnnotations()
.Validate(options => options.Timeout > 0, "Timeout must be positive.");
Разбор:
AddOptions<DatabaseOptions>()начинает цепочку настройки и валидации опций.BindConfiguration("Database")связывает класс с одноимённой секцией в конфигурации.ValidateDataAnnotations()включает проверки атрибутов (Required,Rangeи т.д.).Validate(...)добавляет произвольное бизнес-правило для опций.
- Проверка при старте:
var app = builder.Build();
app.ValidateOptions(); // выбросит исключение при невалидных настройках
Разбор:
- После
builder.Build()приложение уже собрано и готово к запуску. ValidateOptions()принудительно проверяет опции до начала обработки запросов.- При ошибках конфигурации приложение завершится сразу, а не в случайный момент под нагрузкой.
Это гарантирует, что приложение не запустится с некорректной конфигурацией.
Создание кастомных поставщиков
Для специфичных источников (например, конфигурация из базы данных, gRPC-сервиса) можно реализовать свой поставщик.
- Реализация
IConfigurationProvider:
Код ITЗагрузка примера кода…
Разбор:
- Класс наследуется от
ConfigurationProviderи реализует собственный источник настроек. - В
Load()выполняется чтение ключей и значений из базы данных. - Результат помещается в
data, которую используетIConfiguration. - Такой provider позволяет централизовать runtime-конфигурацию вне файлов.
- Реализация
IConfigurationSource:
Код ITЗагрузка примера кода…
Разбор:
IConfigurationSourceотвечает за создание конкретного provider-а.- Класс хранит параметры подключения и передаёт их в
DatabaseConfigurationProvider. - Метод
Build(...)вызывается конфигурационным пайплайном при старте приложения.
- Метод расширения для удобства:
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в стек конфигурации.
- Регистрация:
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>.
Время жизни сервисов
Выбор времени жизни — это архитектурное решение, влияющее на производительность, изоляцию и потокобезопасность.
-
Singleton
- Один экземпляр на всё приложение.
- Когда использовать — логгеры, утилиты без состояния, кэши-обёртки, глобальные конфигурации.
- Опасности:
— Хранение состояния (например,List<T> _items) приведёт к гонкам данных,
— Зависимость от scoped-сервисов (например,DbContext) вызоветInvalidOperationExceptionпри попытке разрешения.
-
Scoped
- Один экземпляр на HTTP-запрос (или на сессию в фоновой задаче).
- Когда использовать:
—DbContext(EF Core требует одного контекста на запрос для отслеживания изменений),
— Сервисы с состоянием, привязанным к запросу (например,ICurrentUserService),
— Unit of Work, репозитории в рамках одного запроса. - Опасности:
— Использование в singleton’ах (см. выше),
— Передача scoped-сервиса в фоновый поток без создания scope’а.
-
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 в различных контекстах
- В контроллерах и Razor Pages
Зависимости инъектируются через конструктор. Фреймворк автоматически разрешает их из DI:
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
}
Разбор:
- Контроллер получает
IProductServiceчерез constructor injection. - Поле
_productServiceхранит зависимость для дальнейшего использования в action-методах. - Такой стиль делает зависимости явными и упрощает unit-тестирование контроллера.
- В middleware
Middleware создаётся один раз при старте приложения (не на каждый запрос!), поэтому зависимости нельзя инъектировать в конструктор — они будут "заморожены" как singleton’ы.
Правильный способ — получать scoped-сервисы черезcontext.RequestServices:
Код ITЗагрузка примера кода…
Разбор:
- Middleware хранит только
_nextв конструкторе и переиспользуется между запросами. - В
InvokeAsync(...)scoped-зависимости безопасно запрашиваются черезcontext.RequestServices. await _next(context)передаёт управление следующему звену pipeline.- Такой подход избегает ошибки "singleton зависит от scoped".
- В фоновых задачах (IHostedService)
IHostedServiceсоздаётся как singleton. Для выполнения scoped-операций нужно вручную создавать scope:
Код ITЗагрузка примера кода…
Разбор:
IHostedServiceзапускается как singleton-фоновый сервис.CreateScope()создаёт отдельный DI-scope для scoped-сервисов внутри фоновой операции.- Через
scope.ServiceProviderбезопасно получаетсяAppDbContext. - Это правильный паттерн для фоновых задач, работающих с БД.
- В 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. Работа с данными в веб-приложении
Слои данных и их границы
В правильно спроектированном веб-приложении данные проходят через несколько слоёв, каждый из которых имеет чёткую ответственность:
-
Transport Layer (HTTP)
— Входящие данные — query string, route parameters, form fields, JSON/XML тело запроса,
— Исходящие данные — JSON, XML, HTML, файлы.
Ответственность: сериализация/десериализация, валидация формата. -
Application Layer (контроллеры, страницы)
— Принимает транспортные объекты (DTO),
— Оркестрирует вызовы сервисов,
— Преобразует результаты в транспортные объекты.
Ответственность: координация, не бизнес-логика. -
Domain Layer (сервисы, агрегаты)
— Содержит бизнес-правила, валидацию предметной области, инварианты,
— Оперирует доменными моделями (rich domain objects),
— Не знает о HTTP, DTO, ORM.
Ответственность: "почему?" — почему операция разрешена/запрещена. -
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.
Источники данных и приоритеты:
- Route Values (
{id}в шаблоне) — высший приоритет для параметров с совпадающими именами, - Query String (
?page=2&size=10), - Form Data (
application/x-www-form-urlencoded,multipart/form-data), - Request Body (
application/json,application/xml) — только для одного параметра (обычно сложного типа), - 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 и политики
Валидация происходит после привязки модели и состоит из двух уровней:
-
Валидация формата (Model State Validation)
— Проверка типов ("abc"→int= ошибка),
— Проверка атрибутовDataAnnotations([Required],[StringLength]),
— Результат сохраняется вModelState.IsValid. -
Бизнес-валидация (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-стек
-
Entity Framework Core (EF Core) — полноценный ORM с поддержкой:
— Отслеживания изменений (change tracking),
— Ленивой загрузки (lazy loading, опционально),
— Миграций кода первой (code-first migrations),
— Поддержки отношений (one-to-many, many-to-many),
— Глобальных фильтров (soft delete),
— Owned Types (вложенные объекты как часть агрегата).
Когда использовать: приложения с богатой доменной моделью, где важна продуктивность разработки и поддержка сложных сценариев. -
Dapper — микро-ORM, выполняющий только маппинг результатов SQL-запросов в объекты.
— Высокая производительность (близка к ADO.NET),
— Полный контроль над SQL,
— Нет отслеживания изменений — нужно писатьUPDATEвручную.
Когда использовать — high-load read-операции, legacy-БД с неудобной схемой, микросервисы с простыми CRUD-операциями. -
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(...)для больших таблиц.
Связанное углубление:
- Identity — JWT и cookie — API, MVC, роли.
- Интеграционные тесты — проверка auth и маршрутов целиком.
- EF Core — первая программа — практический старт по данным.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Веб-разработка — Веб-разработка и API на C#, Документация и практика ASP.NET (Microsoft Learn), C# — о разделе, Приложение с S3, PostgreSQL и ASP.NET Core Web API, ASP.NET - веб-платформа Microsoft, Python — о разделе.