ASP.NET Core (.NET 10): современная разработка MVC и Razor Pages с аутентификацией

Курс учит создавать современные веб-приложения на ASP.NET Core (.NET 10) с использованием MVC и Razor Pages. Вы разберёте архитектуру, работу с данными, построение UI, а также аутентификацию и авторизацию на основе Identity и cookies. Итог — готовое приложение с защищёнными разделами и современным интерфейсом.

1. Старт: .NET 10, структура проекта, middleware и конфигурация

Старт: .NET 10, структура проекта, middleware и конфигурация

В этой статье мы соберём прочный фундамент для всего курса: создадим проект на ASP.NET Core под .NET 10, разберём структуру решения, поймём что реально происходит в Program.cs, как устроен конвейер обработки HTTP-запросов (middleware pipeline) и как работает конфигурация приложения.

В следующих статьях мы будем строить MVC и Razor Pages, подключать аутентификацию и авторизацию, добавлять современные UI-паттерны. Но всё это работает только если вы уверенно понимаете базовые механизмы.

Что понадобится

  • .NET SDK (целевой фреймворк проекта net10.0)
  • Любая среда разработки
  • - Visual Studio - Visual Studio Code - JetBrains Rider

    Полезные справочные ссылки:

  • dotnet new (шаблоны проектов)
  • Middleware в ASP.NET Core
  • Конфигурация в ASP.NET Core
  • Зависимости и DI-контейнер
  • Окружения (Development/Staging/Production)
  • Создаём проект MVC или Razor Pages

    В ASP.NET Core есть два близких подхода к созданию серверного UI:

  • MVC: контроллеры + представления, удобно для приложений с выраженной логикой маршрутизации и контроллерами.
  • Razor Pages: страницы как основной объект, удобно для CRUD и UI, где логика сосредоточена вокруг страниц.
  • Для курса подойдут оба подхода: фундамент одинаковый (хост, middleware, DI, конфигурация, аутентификация). Вы можете выбрать один, а потом при желании сравнить со вторым.

    Создание проекта MVC

    Создание проекта Razor Pages

    После запуска откройте адрес из консоли (обычно http://localhost:xxxx).

    Структура проекта: что где лежит и зачем

    Структура зависит от шаблона (MVC или Razor Pages), но ключевые элементы похожи.

    | Элемент | Где встречается | Зачем нужен | |---|---|---| | Program.cs | всегда | Точка входа: создаёт приложение, настраивает сервисы и middleware | | appsettings.json | всегда | Базовая конфигурация (логирование, ваши настройки) | | appsettings.Development.json | обычно | Настройки только для Development | | Properties/launchSettings.json | часто | Профили запуска, переменные окружения при старте из IDE | | wwwroot/ | часто | Статика: CSS, JS, изображения | | Controllers/ | MVC | Контроллеры | | Views/ | MVC | Razor-представления | | Models/ | MVC (часто) | Модели для UI/валидации/данных | | Pages/ | Razor Pages | Razor-страницы (.cshtml + PageModel) |

    Важная мысль: папки сами по себе “магии” не дают. Магия включается, когда вы добавляете соответствующие сервисы (например, MVC) и включаете маршрутизацию/endpoint’ы.

    Program.cs: минимальная модель хостинга

    Современный ASP.NET Core использует минимальную модель хостинга: всё обычно помещается в Program.cs.

    Ниже пример типового Program.cs (упрощённо). Он почти одинаков для MVC и Razor Pages, отличаются только строки подключения UI-фреймворка.

    Пример: MVC

    Пример: Razor Pages

    Ключевая идея:

  • builder.Services... добавляет возможности приложения (MVC, Razor Pages, аутентификация, базы данных и т.д.).
  • app.Use... задаёт как именно запрос проходит через конвейер.
  • app.Map... описывает какие endpoint’ы доступны (маршруты, страницы).
  • Middleware: конвейер обработки запроса

    Middleware — это компоненты, которые обрабатывают каждый HTTP-запрос по цепочке. Каждый middleware может:

  • сделать работу до следующего middleware
  • вызвать следующий middleware
  • сделать работу после следующего middleware
  • завершить запрос сразу (не передавая дальше)
  • !Диаграмма конвейера middleware и критичность порядка

    Официальная документация: Middleware в ASP.NET Core

    Почему порядок Use... критичен

    Потому что один middleware часто подготавливает данные для другого.

    Типичный порядок для приложений с UI и безопасностью:

  • UseExceptionHandler или UseDeveloperExceptionPage (в зависимости от окружения)
  • UseHsts (обычно только не в Development)
  • UseHttpsRedirection
  • UseStaticFiles
  • UseRouting
  • UseAuthentication
  • UseAuthorization
  • Map... (эндпоинты MVC/Razor Pages)
  • Пример логики:

  • Если вы поставите UseStaticFiles после Map..., статические файлы могут не отдаваться так, как ожидается.
  • Если вы поставите UseAuthorization до UseAuthentication, то авторизация не сможет опереться на установленного пользователя.
  • Endpoint routing: UseRouting и Map...

  • app.UseRouting() включает механизм сопоставления запроса с endpoint’ом.
  • app.MapControllerRoute(...) или app.MapRazorPages() объявляет, какие endpoint’ы существуют.
  • Важно: Map... обычно размещают ближе к концу, когда уже подключены нужные middleware (например, аутентификация).

    Окружения: Development, Staging, Production

    ASP.NET Core использует окружения, чтобы включать разные настройки и поведение.

  • Development: максимум диагностик, подробные ошибки
  • Production: безопасные ошибки, HSTS, минимум лишней информации
  • В коде это видно через app.Environment.IsDevelopment().

    Справка: Окружения в ASP.NET Core

    Конфигурация: откуда берутся настройки

    ASP.NET Core собирает конфигурацию из нескольких источников (providers). На практике чаще всего используются:

  • appsettings.json
  • appsettings.{Environment}.json (например, appsettings.Development.json)
  • переменные окружения
  • секреты разработки (User Secrets)
  • параметры командной строки
  • Справка: Конфигурация в ASP.NET Core

    Пример: добавим свою настройку в appsettings.json

    appsettings.json:

    Чтение напрямую из конфигурации (быстро, но не всегда идеально для больших проектов):

    Замечания:

  • builder.Configuration["App:BrandName"] возвращает строку или null.
  • Для сложных настроек лучше использовать Options pattern.
  • Options pattern: типобезопасные настройки

    Создадим класс настроек:

    Зарегистрируем в DI и свяжем с секцией App:

    Что это даёт:

  • настройки описаны классом
  • их удобно внедрять через DI
  • меньше строковых ключей (меньше ошибок)
  • Dependency Injection: как подключаются сервисы

    ASP.NET Core из коробки использует DI-контейнер. Вы регистрируете зависимости в builder.Services, а затем получаете их:

  • в конструкторах контроллеров и PageModel
  • в middleware
  • в endpoint’ах (MapGet, MapPost)
  • Справка: Dependency injection в ASP.NET Core

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

    Чаще всего используются:

  • Singleton: один экземпляр на всё приложение
  • Scoped: один экземпляр на запрос
  • Transient: новый экземпляр каждый раз
  • Важно заранее понимать: контекст запроса (например, многие вещи вокруг аутентификации и работы с данными) обычно требует Scoped.

    Статика, ошибки, HTTPS: базовые middleware “по умолчанию”

  • UseHttpsRedirection перенаправляет HTTP на HTTPS.
  • UseStaticFiles включает раздачу файлов из wwwroot.
  • UseExceptionHandler задаёт безопасную обработку ошибок в Production.
  • UseHsts добавляет HSTS-заголовок (в Production).
  • Документация:

  • Статические файлы
  • Обработка ошибок
  • Итог

    Вы разобрали основу, на которой строится всё остальное:

  • как создать проект MVC или Razor Pages под .NET 10
  • за что отвечает Program.cs в минимальной модели хостинга
  • что такое middleware и почему порядок вызовов важен
  • как устроена конфигурация (appsettings.json, окружения, providers)
  • как использовать DI и Options pattern для типобезопасных настроек
  • В следующей статье логично перейти к созданию страниц/контроллеров, маршрутизации, layout’ам и подготовить приложение к подключению аутентификации и авторизации.

    2. MVC и Razor Pages: маршрутизация, контроллеры, страницы и модели

    MVC и Razor Pages: маршрутизация, контроллеры, страницы и модели

    В прошлой статье вы собрали фундамент: поняли Program.cs, middleware-конвейер, DI и конфигурацию. Теперь переходим к тому, как приложение отвечает на конкретные URL: разберём маршрутизацию, базовые механики MVC и Razor Pages, а также модели, привязку данных и валидацию.

    Цель этой статьи: чтобы вы могли уверенно объяснить, почему запрос GET /products/5 попадает в конкретный обработчик, откуда берутся параметры, как они валидируются и как формируется HTML-ответ.

    Как запрос попадает в ваш код

    ASP.NET Core использует endpoint routing: вы описываете, какие endpoint’ы существуют (MVC маршруты, Razor Pages, минимальные API), а затем платформа находит подходящий endpoint для текущего HTTP-запроса.

    Типовая схема (упрощённо):

  • app.UseRouting() включает сопоставление запроса с endpoint’ом.
  • Middleware аутентификации и авторизации (позже в курсе) используют найденный endpoint и его метаданные.
  • app.Map... регистрирует endpoint’ы: контроллеры или страницы.
  • !Диаграмма, показывающая где в пайплайне происходит маршрутизация и почему аутентификация/авторизация ставятся после UseRouting

    Документация: Маршрутизация в ASP.NET Core

    Маршрутизация в MVC и Razor Pages

    Конвенциональная маршрутизация MVC

    Конвенциональная маршрутизация задаётся в Program.cs через MapControllerRoute.

    Как читать шаблон:

  • {controller=Home}: если сегмент отсутствует, берётся Home
  • {action=Index}: если сегмент отсутствует, берётся Index
  • {id?}: необязательный параметр
  • Пример сопоставления:

  • GET /HomeController.Index()
  • GET /Products/Details/5ProductsController.Details(id: 5)
  • Плюс конвенций: удобно стартовать и поддерживать единый стиль URL. Минус: сложнее выразить нестандартные URL без дополнительных правил.

    Атрибутная маршрутизация MVC

    Атрибутная маршрутизация описывается прямо над контроллером и методами.

    Что важно:

  • [Route("products")] задаёт общий префикс
  • [HttpGet("{id:int}")] добавляет сегмент с ограничением типа int
  • ограничение :int защищает от нежелательных совпадений (например, /products/abc)
  • Документация: Атрибутная маршрутизация

    Маршрутизация Razor Pages

    Razor Pages маршрутизируются по файловой структуре папки Pages.

  • Pages/Index.cshtml/
  • Pages/Products/Index.cshtml/Products
  • Pages/Products/Create.cshtml/Products/Create
  • Чтобы страница стала endpoint’ом, в начале .cshtml должен быть @page.

    Можно задавать шаблон маршрута прямо в @page:

    Тогда Pages/Products/Details.cshtml начнёт принимать /Products/Details/5 (если страница лежит в Pages/Products) или, при иной структуре, маршрут будет строиться от папки.

    Документация: Razor Pages: маршрутизация

    MVC: контроллеры, действия и представления

    Контроллер и action

    Контроллер в MVC — класс, который группирует обработчики запросов (actions). Обычно наследуется от Controller.

    IActionResult — общий тип результата. На практике вы часто возвращаете:

  • View() — отрисовать Razor-представление
  • RedirectToAction(...) — перенаправить
  • NotFound() — 404
  • BadRequest() — 400
  • Документация: Обзор контроллеров

    Представления (Views) и общий layout

    MVC представления лежат в Views/<Controller>/<Action>.cshtml. Обычно используется общий шаблон:

  • Views/Shared/_Layout.cshtml — каркас страницы
  • Views/_ViewImports.cshtml — подключение Tag Helpers и общих using
  • Views/_ViewStart.cshtml — указание layout по умолчанию
  • Tag Helpers: современная генерация HTML

    Tag Helpers помогают генерировать корректные URL и связывать форму с моделью.

    Пример ссылки на action:

    Плюсы:

  • URL формируется по маршрутам, а не захардкоженной строкой
  • при изменении маршрутизации ссылки не «ломаются» так часто
  • Документация: Tag Helpers в ASP.NET Core

    Модели, привязка данных и валидация

    Модель и Data Annotations

    В UI-проектах модель часто несёт данные формы и правила валидации.

    Что делают атрибуты:

  • [Required] запрещает пустое значение
  • [StringLength] задаёт максимальную длину
  • [Range] задаёт допустимый диапазон
  • Документация: Валидация моделей

    Model binding: откуда берутся значения параметров

    Model binding — это механизм, который заполняет параметры action’а или свойства PageModel значениями из запроса.

    Основные источники:

  • route values: параметры из URL-шаблона (/products/5id = 5)
  • query string: ?page=2
  • form fields: значения из HTML-формы (POST)
  • В MVC вы можете явно указать источники:

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

    ModelState и обработка ошибок в форме

    После привязки и валидации MVC заполняет ModelState. Типовой паттерн для формы:

    В представлении удобно показывать ошибки так:

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

    Пример: небольшой MVC-модуль (без базы)

    Чтобы сфокусироваться на механике MVC, сделаем простейший репозиторий в памяти и несколько действий.

    Модель данных (для примера):

    Контракт репозитория и реализация:

    Регистрация в DI:

    Контроллер:

    Ключевые выводы из примера:

  • параметры id и ProductInput input заполняются автоматически через model binding
  • валидация запускается автоматически по Data Annotations
  • ModelState.IsValid решает, показывать ли форму с ошибками или продолжать обработку
  • Razor Pages: страницы, PageModel и handlers

    Razor Pages — это подход, где страница является основным объектом. Обычно вы держите рядом:

  • Index.cshtml — разметка
  • Index.cshtml.cs — класс PageModel с обработчиками
  • PageModel и обработчики

    Типовые обработчики:

  • OnGet или OnGetAsync — для GET
  • OnPost или OnPostAsync — для POST
  • Можно иметь несколько POST-обработчиков через именованные handlers:

  • OnPostSaveAsync
  • OnPostDeleteAsync
  • Выбор handler’а делается через параметр handler и Tag Helpers.

    Документация: Razor Pages: обработчики

    Пример Razor Pages: создание продукта

    Pages/Products/Create.cshtml:

    Pages/Products/Create.cshtml.cs:

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

  • [BindProperty] включает привязку данных формы к свойству Input
  • return Page() возвращает текущую страницу с заполненными ошибками
  • RedirectToPage(...) делает перенаправление на другую Razor Page
  • Документация: BindProperty

    ViewModel, InputModel и разделение ответственности

    В реальном приложении полезно разделять:

  • доменную модель (сущность): например, Product как бизнес-объект
  • модель ввода (input): например, ProductInput с атрибутами валидации формы
  • модель представления (view model): например, ProductsListVm, где уже подготовлены данные для конкретного экрана
  • Почему это важно:

  • UI-валидация и доменная логика не всегда совпадают
  • формы часто требуют полей, которых нет в сущности (подтверждение пароля, чекбоксы согласия)
  • проще контролировать, какие поля разрешено принимать из запроса
  • Документация: Рекомендации по моделям в MVC

    Что выбрать: MVC или Razor Pages

    Оба подхода используют одинаковый фундамент (DI, middleware, конфигурация, аутентификация). Отличия в организации UI.

    Таблица для выбора:

    | Критерий | MVC | Razor Pages | |---|---|---| | Основная единица | контроллер + actions | страница + handlers | | Удобно для | сложной маршрутизации, группировки по контроллерам, смешанных сценариев UI + API | CRUD и page-centric UI, когда логика рядом со страницей | | Маршруты | конвенции и атрибуты | по папкам + @page шаблоны | | Читаемость | хорошо, когда много связанных actions | хорошо, когда всё вокруг одной страницы |

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

  • если вы строите классическое приложение с множеством связанных операций и нестандартными маршрутами, MVC часто проще расширять
  • если вы делаете административку, личный кабинет, CRUD-экраны и хотите максимум связности «страница + логика», Razor Pages дают очень чистую структуру
  • Итог

    Вы научились:

  • понимать, как UseRouting и Map... превращают URL в конкретный endpoint
  • использовать конвенциональную и атрибутную маршрутизацию в MVC
  • понимать файловую маршрутизацию Razor Pages и роль @page
  • применять model binding и Data Annotations валидацию
  • строить формы и выводить ошибки через Tag Helpers
  • Следующий логичный шаг курса: подключить аутентификацию и авторизацию и увидеть, как выбранные маршруты, контроллеры и страницы начинают защищаться правилами доступа.

    3. Данные: EF Core, миграции, связи, репозитории и валидация

    Данные: EF Core, миграции, связи, репозитории и валидация

    В предыдущих статьях вы разобрали фундамент ASP.NET Core: Program.cs, middleware-конвейер, DI, конфигурацию, а затем научились строить UI на MVC или Razor Pages с маршрутизацией, model binding и валидацией форм. Теперь добавим постоянное хранение данных.

    В этой статье вы научитесь:

  • подключать EF Core к ASP.NET Core приложению
  • описывать сущности и связи (один-ко-многим, многие-ко-многим)
  • делать миграции и обновлять базу
  • проектировать слой доступа к данным через репозитории без лишней абстракции
  • выстраивать валидацию на уровне UI и базы (ограничения, уникальность)
  • Основные ссылки:

  • Обзор EF Core
  • Конфигурация DbContext и DI
  • Миграции EF Core
  • Связи в EF Core
  • Валидация моделей в ASP.NET Core
  • Зачем EF Core в приложении MVC или Razor Pages

    EF Core решает задачу отображения объектов .NET на таблицы базы данных и обратно:

  • вы работаете с DbContext и DbSet<T> как с коллекциями
  • EF Core строит SQL-запросы и выполняет их
  • миграции синхронизируют модель и схему базы
  • Важно: EF Core не заменяет понимание SQL и индексов, но позволяет быстро и безопасно развивать приложение.

    Выбор провайдера базы данных для курса

    Для обучения удобно начать со SQLite:

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

    Подключение EF Core и создание DbContext

    Установка пакетов

    Для SQLite обычно нужны пакеты:

    Design нужен для миграций и инструментов.

    Строка подключения

    Добавьте в appsettings.json:

    DbContext

    Создадим контекст приложения AppDbContext.

    Ключевые моменты:

  • DbContextOptions<AppDbContext> передаются через DI
  • DbSet<T> соответствует таблице
  • OnModelCreating настраивает связи и ограничения на уровне модели EF
  • Регистрация в DI

    В Program.cs:

    AddDbContext регистрирует контекст как Scoped, то есть один экземпляр на HTTP-запрос. Это соответствует жизненному циклу большинства веб-сценариев.

    Сущности и связи

    Будем моделировать каталог товаров:

  • Category содержит много Product
  • Product принадлежит одной Category
  • у продукта есть уникальный Sku
  • !Связь один-ко-многим между категориями и товарами

    Сущности

    Пояснения:

  • CategoryId это внешний ключ
  • Category это навигационное свойство для удобного доступа
  • Data Annotations здесь выполняют двойную роль:
  • - помогают UI-валидации - частично влияют на модель EF Core (например, длины строк)

    Конфигурация связей: Data Annotations и Fluent API

    У вас есть два базовых способа:

  • атрибуты (Data Annotations)
  • Fluent API в OnModelCreating
  • Практическое правило:

  • простые ограничения можно держать в атрибутах
  • связи, индексы, правила удаления и более сложные настройки лучше задавать Fluent API
  • Миграции: как создать и обновить базу

    Миграции это контролируемые изменения схемы базы данных, которые EF Core генерирует на основании изменений модели.

    Установка инструмента

    Если dotnet-ef ещё не установлен:

    Проверка:

    Создание миграции

    Находясь в папке проекта (где .csproj):

    EF Core создаст папку Migrations с кодом миграции и снимком модели.

    Применение миграции к базе

    Если используется SQLite со строкой Data Source=app.db, рядом появится файл app.db.

    Что важно понимать про миграции

  • Миграции это часть исходного кода и истории проекта.
  • Для Production обычно применяют миграции через пайплайн деплоя.
  • При командной работе миграции могут конфликтовать и требуют дисциплины.
  • Запросы EF Core: чтение и запись данных

    Базовые операции

    Пример сервиса, который добавляет категорию и продукт:

    Пояснения:

  • SaveChangesAsync фиксирует изменения в базе
  • AsNoTracking() полезен для чтения в UI: меньше накладных расходов, если вы не собираетесь редактировать сущности
  • CancellationToken хорошо передавать во все операции I/O
  • Загрузка связанных данных

    Если вам нужно вывести категорию вместе с товарами:

    Include говорит EF Core загрузить связанные данные одним запросом (или несколькими, в зависимости от провайдера и ситуации).

    Связь многие-ко-многим на практике

    Рассмотрим теги для продуктов:

  • у продукта много тегов
  • у тега много продуктов
  • Сущности

    Настройка

    EF Core создаст таблицу связей ProductTags автоматически.

    После добавления этих сущностей и конфигурации потребуется новая миграция.

    Репозитории: как сделать слой данных и не навредить

    EF Core сам по себе уже реализует паттерны Repository и Unit of Work через DbContext. Поэтому репозитории поверх EF Core стоит делать осознанно, чтобы не получить:

  • лишний слой абстракции без выгоды
  • утечку IQueryable в UI без контроля
  • проблему тестирования из-за слишком «умных» репозиториев
  • Практичная цель репозитория в UI-приложении:

  • спрятать детали запросов и Include
  • централизовать правила чтения (например, AsNoTracking)
  • ограничить набор доступных операций
  • Пример: репозиторий продуктов

    Контракт:

    Реализация:

    Регистрация в DI:

    Обратите внимание:

  • чтение сделано через AsNoTracking, а GetAsync оставлен tracking, потому что часто это чтение «под редактирование»
  • SaveChangesAsync оставлен отдельно, чтобы можно было добавить несколько сущностей и сохранить одним коммитом
  • Валидация: UI-уровень и уровень базы

    Правильная стратегия это несколько уровней защиты:

  • валидация ввода в UI (MVC или Razor Pages)
  • ограничения модели EF Core и миграции
  • ограничения базы данных (NOT NULL, UNIQUE, FOREIGN KEY)
  • обработка исключений при сохранении
  • Валидация в MVC и Razor Pages

    Вы уже использовали Data Annotations и ModelState. Для операций записи типичный поток один и тот же.

    MVC:

    Razor Pages:

    InputModel отдельно от сущности

    Для форм лучше использовать отдельную модель ввода, чтобы:

  • не принимать лишние поля от пользователя
  • изолировать UI-валидацию от доменной модели
  • Уникальность и ошибки сохранения

    Даже если вы проверили уникальность Sku в коде, между проверкой и SaveChangesAsync другой запрос может вставить такой же SKU. Это нормальная ситуация конкурентного доступа.

    Практика:

  • уникальность задавать индексом IsUnique()
  • при DbUpdateException показывать понятное сообщение
  • Полезные практики EF Core для веб-приложений

  • Используйте AsNoTracking() для списков и страниц просмотра.
  • Держите DbContext как Scoped и не сохраняйте его в Singleton сервисах.
  • Делайте запросы явными: где нужны связи, используйте Include или проекции через Select.
  • Не отдавайте сущности напрямую во внешние контракты, если они не являются вашим UI-контрактом.
  • Итог

    Теперь у вас есть полный базовый контур работы с данными в ASP.NET Core приложении:

  • EF Core подключён через DI и конфигурацию
  • сущности и связи описаны в модели
  • миграции создают и обновляют схему базы
  • репозитории дают управляемый слой доступа к данным
  • валидация работает на уровне UI и базы, а ошибки сохранения корректно обрабатываются
  • Следующий логичный шаг курса: подключить аутентификацию и авторизацию и применить её к операциям чтения и записи данных, чтобы защитить страницы и действия, связанные с каталогом.

    4. Современный UI: Bootstrap, компоненты, формы, ошибки и UX

    Современный UI: Bootstrap, компоненты, формы, ошибки и UX

    В предыдущих статьях вы построили фундамент ASP.NET Core приложения: разобрали middleware и конфигурацию, научились делать страницы на MVC или Razor Pages, подключили EF Core и миграции. Теперь доведём интерфейс до современного уровня: единый layout, аккуратные компоненты, удобные формы, понятные ошибки и базовые UX-практики.

    Цель статьи: чтобы ваш UI выглядел профессионально, а пользовательский ввод обрабатывался предсказуемо и дружелюбно.

    Что такое современный UI в серверном ASP.NET Core

    В контексте MVC и Razor Pages это обычно означает:

  • единый визуальный стиль на всём сайте
  • адаптивность под мобильные и широкие экраны
  • понятные состояния интерфейса: успех, ошибка, пусто, загрузка
  • правильные формы: подсказки, валидация, сохранение введённых данных
  • доступность: корректные подписи, читаемость, фокус, контраст
  • Bootstrap закрывает большую часть задачи, а ASP.NET Core даёт удобные механизмы генерации ссылок, форм и сообщений об ошибках.

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

  • Bootstrap: документация
  • Tag Helpers в ASP.NET Core
  • Клиентская валидация
  • Обработка ошибок в ASP.NET Core
  • Статические файлы
  • Подключение Bootstrap

    Есть три практичных способа, от простого к более контролируемому.

    CDN

    Подходит для обучения и прототипов.

  • Вы подключаете CSS и JS из публичного CDN.
  • При первом рендере страница загрузит ресурсы из сети.
  • Пример фрагмента layout (показан текстом, чтобы не вставлять HTML-теги):

    Практический нюанс:

  • для продакшена часто выбирают локальные файлы из-за контроля версий, CSP и независимости от внешних сервисов
  • Локальные файлы в wwwroot

    Подходит для продакшена и корпоративных сред.

  • Скачайте дистрибутив Bootstrap.
  • Положите bootstrap.min.css и bootstrap.bundle.min.js в wwwroot/lib/bootstrap/.
  • Подключайте через относительные пути.
  • Это работает только если включена раздача статики:

  • app.UseStaticFiles() должен быть в middleware-конвейере (вы это разбирали в первой статье)
  • LibMan

    Это компромисс между ручным скачиванием и npm.

  • Library Manager (LibMan)
  • Смысл:

  • библиотека скачивается из публичного источника и кладётся в wwwroot
  • вы фиксируете версии в файле конфигурации LibMan
  • Layout как центр UI: единая рамка страниц

    И в MVC, и в Razor Pages ключевая идея одинаковая:

  • есть общий layout, который задаёт структуру страницы
  • конкретная страница подставляет содержимое в область рендера
  • Где искать layout

  • MVC: Views/Shared/_Layout.cshtml
  • Razor Pages: Pages/Shared/_Layout.cshtml
  • Также полезны:

  • MVC: Views/_ViewStart.cshtml и Views/_ViewImports.cshtml
  • Razor Pages: Pages/_ViewImports.cshtml
  • Что обычно лежит в layout

  • подключение Bootstrap
  • контейнер и сетка
  • навигация
  • место для системных сообщений
  • RenderBody и при необходимости секции для скриптов
  • Минимальная структура (опять же показано текстом):

    Компоненты Bootstrap, которые дают максимум пользы

    Bootstrap ценен не только сеткой, но и готовыми UI-паттернами. Для MVC/Razor Pages особенно полезны следующие.

    Контейнеры и сетка

    Базовый принцип:

  • container ограничивает ширину и задаёт читаемые поля
  • row и col-* формируют сетку
  • Практика:

  • формы редактирования часто удобно держать в колонке col-12 col-lg-6
  • списки и таблицы можно растягивать на всю ширину col-12
  • Навигация

    navbar обычно живёт в layout и даёт:

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

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

    card удобен для:

  • страницы деталей
  • пустых состояний
  • группировки блока формы
  • Таблицы и списки

    Списки товаров и категорий часто делаются так:

  • таблица с table и table-striped
  • действия в последней колонке: Details, Edit, Delete
  • Практический совет:

  • для мобильных экранов таблица может быть слишком широкой, поэтому добавляют обёртку table-responsive
  • Алерты

    alert отлично подходит под сообщения:

  • успешное сохранение
  • предупреждение
  • ошибка
  • Мы ниже свяжем это с TempData.

    !Поток обработки формы: валидация, возврат ошибок, редирект после успеха

    Формы: связка Bootstrap и model binding

    Сильная сторона ASP.NET Core в том, что вы описываете правила на модели (Data Annotations), а UI автоматически:

  • привязывает поля
  • показывает ошибки
  • сохраняет введённые значения при возврате формы
  • Соглашение: отдельная InputModel

    Вы уже касались этого в статье про EF Core. Для UI это критично:

  • сущность базы и модель формы часто не совпадают
  • вы контролируете, какие поля реально принимает сервер
  • Пример модели ввода:

    Bootstrap-стили для инпутов

    Смысл простой:

  • текстовые поля получают класс form-control
  • селекты получают класс form-select
  • кнопки получают btn и конкретный вариант, например btn-primary
  • Отображение ошибок рядом с полем

    В MVC и Razor Pages вы обычно делаете три вещи:

  • выводите общий блок ошибок формы
  • выводите ошибку рядом с каждым полем
  • подсвечиваете поле как невалидное
  • Концептуальный фрагмент разметки с Tag Helpers (показано текстом):

    Ключевая связка:

  • asp-for берёт имя поля и текущее значение из модели
  • asp-validation-for показывает сообщения из ModelState
  • asp-validation-summary показывает ошибки уровня модели, например конфликт уникального SKU
  • Подсветка невалидных полей

    Есть два распространённых подхода.

  • Положиться на стандартную интеграцию ASP.NET Core с unobtrusive validation и встроенные стили.
  • Явно добавлять классы is-invalid и is-valid на основе ModelState.
  • Второй вариант полезен, когда вы хотите полностью контролировать внешний вид.

    Пример серверного решения на стороне MVC представления (логика на C#):

    Дальше этот класс подставляется в поле.

    Клиентская валидация

    Клиентская валидация улучшает UX:

  • пользователь видит ошибку сразу
  • уменьшается число лишних POST-запросов
  • Но важное правило:

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

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

  • Клиентская валидация
  • Сообщения об успехе и ошибке: UX без лишней сложности

    PRG: Post-Redirect-Get

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

    Поэтому стандартный UX-паттерн:

  • POST сохраняет данные
  • затем делает redirect
  • на GET отображается страница результата
  • В MVC это обычно RedirectToAction, в Razor Pages это RedirectToPage.

    TempData для одноразовых уведомлений

    TempData хранит данные до следующего запроса и идеально подходит для алертов.

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

  • TempData и состояние приложения
  • Пример для MVC контроллера:

    Идея вывода в layout (показано текстом):

    Практика:

  • success для успешных операций
  • warning для предупреждений
  • danger для ошибок
  • Ошибки уровня модели

    Пример: уникальный индекс SKU в базе. Даже если вы проверяете это заранее, может случиться гонка.

    Правильный UX:

  • поймать ошибку сохранения
  • добавить сообщение в ModelState как общую ошибку
  • вернуть форму с сохранёнными значениями
  • Пример:

    Страницы ошибок и корректные статусы

    Пользователь не должен видеть технические детали. В продакшене вы включаете безопасную обработку ошибок (вы это уже видели в первой статье).

    Единая страница ошибки

    Обычно используют:

  • UseExceptionHandler("/Home/Error") для MVC
  • UseExceptionHandler("/Error") для Razor Pages
  • Документация:

  • Обработка ошибок
  • 404 и другие статус-коды

    Для UX полезно иметь красивую страницу не только для исключений, но и для 404.

    Типовой вариант:

  • UseStatusCodePagesWithReExecute перенаправляет на ваш endpoint, сохраняя статус-код
  • Это позволяет сделать страницу вида:

  • 404: ресурс не найден
  • 403: доступ запрещён (особенно актуально после подключения авторизации)
  • Минимальные UX-правила для MVC и Razor Pages

    Эти пункты дают большой прирост качества при небольших усилиях.

  • Всегда возвращайте пользователя на понятную страницу после действия: список или детали.
  • На успешный POST делайте redirect, а не отдавайте HTML напрямую.
  • Показывайте ошибки рядом с полем и общий смысл ошибки вверху.
  • Сохраняйте введённые значения при ошибках валидации, не заставляйте вводить заново.
  • Подписывайте поля понятными названиями и добавляйте подсказки только там, где они нужны.
  • Для опасных действий добавляйте явное подтверждение и отдельный стиль кнопки, например btn-danger.
  • Думайте про пустые состояния: когда список пуст, покажите объяснение и кнопку создания.
  • Как это связывается с аутентификацией и авторизацией

    В следующих материалах, когда вы добавите аутентификацию, UI почти всегда расширяется так:

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

    Итог

    Вы собрали основу современного UI для ASP.NET Core MVC или Razor Pages:

  • подключили Bootstrap осознанными способами
  • поняли роль layout и общих partial
  • научились строить формы с понятной валидацией и подсветкой ошибок
  • внедрили UX-паттерны PRG и flash-сообщения через TempData
  • подготовили проект к корректным страницам ошибок и будущей авторизации
  • 5. Аутентификация: ASP.NET Core Identity, cookies, login/logout, 2FA

    Аутентификация: ASP.NET Core Identity, cookies, login/logout, 2FA

    В прошлых статьях вы построили основу приложения на ASP.NET Core (.NET 10): разобрали конвейер middleware и конфигурацию, научились делать UI на MVC или Razor Pages, подключили EF Core и миграции, привели интерфейс к современному виду (Bootstrap, формы, ошибки и UX). Теперь добавим ключевую часть любого реального веб-приложения: аутентификацию.

    В этой статье вы научитесь:

  • отличать аутентификацию от авторизации
  • подключать ASP.NET Core Identity к проекту MVC или Razor Pages
  • понимать, как работает cookies-аутентификация в ASP.NET Core
  • реализовывать сценарии login/logout
  • включать и объяснять 2FA (двухфакторную аутентификацию)
  • Полезные официальные источники:

  • Обзор ASP.NET Core Identity
  • Cookies-аутентификация
  • Включение 2FA в ASP.NET Core Identity
  • Scaffold Identity (генерация страниц аккаунта)
  • Авторизация в ASP.NET Core
  • Базовые понятия: аутентификация и авторизация

    Эти два термина часто путают, поэтому зафиксируем:

  • Аутентификация отвечает на вопрос: кто ты?
  • Авторизация отвечает на вопрос: что тебе разрешено?
  • Практически это выглядит так:

  • аутентификация создаёт HttpContext.User (набор claims, включая идентификатор пользователя)
  • авторизация применяет правила доступа (например, [Authorize]) к текущему User
  • !Где в пайплайне выполняются аутентификация и авторизация

    Почему Identity, и что он даёт

    ASP.NET Core Identity это готовая система управления пользователями, которая обычно включает:

  • таблицы пользователей, ролей, логинов внешних провайдеров
  • хеширование паролей и политики сложности
  • подтверждение email, сброс пароля
  • блокировки при многократных ошибках
  • 2FA (аутентификатор, SMS или email, recovery codes)
  • интеграцию с cookies-аутентификацией
  • Identity можно использовать и в MVC, и в Razor Pages. Даже если вы пишете MVC-приложение, встроенный UI Identity реализован как Razor Pages и подключается как Area.

    Как работает cookies-аутентификация в ASP.NET Core

    Суть механики:

  • после успешного логина сервер создаёт auth cookie
  • браузер хранит cookie и отправляет его на каждый следующий запрос
  • middleware cookies-аутентификации читает cookie и восстанавливает ClaimsPrincipal в HttpContext.User
  • Важно понимать два момента:

  • cookie обычно содержит не “пароль”, а защищённый тикет с идентификатором пользователя и claims
  • защита cookie опирается на Data Protection (ключи шифрования/подписи), поэтому корректная настройка окружения важна
  • Документация: Cookies-аутентификация

    Подключаем Identity к проекту MVC или Razor Pages

    Ниже показан типовой путь для приложения, где вы уже используете EF Core (как в статье про данные).

    Пакеты

    Если у вас ещё нет Identity-пакетов, обычно достаточно добавить:

    Если вы создавали проект из шаблона с Individual Accounts, многое уже настроено. Если нет, выполняйте шаги ниже.

    DbContext для Identity

    Есть два распространённых варианта:

  • отдельный контекст только для Identity
  • один общий контекст приложения, который одновременно хранит и ваши сущности, и таблицы Identity
  • Для учебного проекта удобен второй вариант.

    Создайте контекст, наследующийся от IdentityDbContext:

    Ключевой момент: обязательно вызвать base.OnModelCreating(modelBuilder), иначе таблицы Identity могут сконфигурироваться некорректно.

    Регистрация сервисов Identity и EF Core

    Пример для Program.cs (подходит и для MVC, и для Razor Pages):

    Почему одновременно и AddControllersWithViews(), и AddRazorPages()?

  • ваше приложение может оставаться MVC
  • а встроенные страницы Identity (логин, регистрация, 2FA) доступны только если включены Razor Pages и выполнен MapRazorPages()
  • Миграции для таблиц Identity

    После перехода на IdentityDbContext в модели появились новые таблицы (например, AspNetUsers). Их нужно применить в базе.

    Обычно шаги такие:

  • Создать миграцию.
  • Применить миграцию к базе.
  • Если вы уже делали миграции для ваших сущностей, это будет просто следующая миграция в истории.

    Страницы login/logout и навигация в UI

    После AddDefaultIdentity и MapRazorPages() у вас появляются маршруты Identity UI:

  • /Identity/Account/Register
  • /Identity/Account/Login
  • /Identity/Account/Logout
  • /Identity/Account/Manage (управление аккаунтом)
  • Ссылки в layout

    В вашем _Layout.cshtml (MVC или Razor Pages) обычно добавляют ссылки:

  • Login
  • Register
  • Logout
  • Manage
  • Их удобно отображать условно, в зависимости от User.Identity.IsAuthenticated.

    В MVC-представлении или Razor Page это одно и то же API:

  • User.Identity?.IsAuthenticated
  • User.Identity?.Name
  • Когда нужно Scaffold Identity

    Встроенные страницы Identity UI подходят многим проектам, но почти всегда хочется:

  • изменить верстку под ваш Bootstrap layout
  • добавить свои поля регистрации
  • поменять тексты и логику
  • Для этого используют scaffold: страницы Identity копируются в ваш проект, и вы их редактируете.

    Документация: Scaffold Identity (генерация страниц аккаунта)

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

  • вы получаете контролируемый код логина/регистрации/2FA
  • вы можете добавить свои требования UX (flash-сообщения, единые ошибки, поля)
  • Login/logout на уровне кода: UserManager и SignInManager

    Identity предоставляет два ключевых сервиса:

  • UserManager<TUser> управляет пользователем (создать, найти, поменять пароль, включить 2FA)
  • SignInManager<TUser> отвечает за вход/выход (создание cookie, проверка пароля, 2FA)
  • Даже если вы используете встроенные страницы, понимать эти сервисы важно: именно они реализуют основную логику.

    Пример: минимальный logout в MVC (обычно это POST, чтобы сложнее было случайно “разлогинить” пользователя ссылкой):

    Что делает SignOutAsync():

  • удаляет (или делает недействительной) auth cookie
  • в следующем запросе HttpContext.User будет анонимным
  • Настройка cookie-поведения: срок жизни, безопасность, пути

    Identity использует cookies-аутентификацию под капотом. Иногда вам нужно явно настроить поведение cookie.

    Пример настройки cookie:

    Пояснения:

  • LoginPath куда перенаправлять анонимного пользователя, если он попал на защищённую страницу
  • AccessDeniedPath куда перенаправлять, если пользователь вошёл, но прав не хватает
  • ExpireTimeSpan максимальный срок жизни cookie
  • SlidingExpiration “продлевает” cookie при активности пользователя
  • HttpOnly защищает cookie от чтения через JavaScript
  • SecurePolicy.Always отправляет cookie только по HTTPS
  • SameSite влияет на отправку cookie в кросс-сайтовых запросах
  • Двухфакторная аутентификация (2FA)

    2FA добавляет второй шаг входа: помимо пароля нужен код из другого источника. Это сильно снижает риск захвата аккаунта при утечке пароля.

    В Identity наиболее распространённый путь для учебного и реального проекта:

  • приложение-аутентификатор (TOTP): Google Authenticator, Microsoft Authenticator и аналоги
  • резервные коды (recovery codes)
  • Документация: Включение 2FA в ASP.NET Core Identity

    Как выглядит поток 2FA

    На уровне сценария:

  • Пользователь вводит email и пароль.
  • Если пароль верный и 2FA включена, система не логинит сразу.
  • Пользователь вводит одноразовый код из приложения-аутентификатора.
  • Только после этого создаётся auth cookie.
  • !Поток входа с 2FA в Identity

    Включение 2FA в UI

    Если вы используете встроенные страницы Identity UI, то после scaffold (или даже без него) в разделе управления аккаунтом обычно доступны:

  • включение приложения-аутентификатора
  • генерация recovery codes
  • отключение 2FA
  • Технически это означает:

  • Identity сохраняет “факт включения 2FA” у пользователя
  • хранит ключ для генерации/проверки TOTP
  • валидирует коды при входе через SignInManager
  • Recovery codes

    Recovery codes нужны, когда пользователь потерял доступ к телефону. Практика:

  • сгенерировать несколько одноразовых кодов
  • показать их пользователю один раз
  • попросить сохранить в безопасном месте
  • Recovery codes нельзя “угадать” на практике, но их можно потерять, поэтому UX обычно объясняет пользователю, что это критично.

    Что важно для безопасности в реальном приложении

    Аутентификация это место, где ошибки стоят дорого. Минимальный набор практик:

  • включайте HTTPS (в шаблонах он обычно включён)
  • используйте CookieSecurePolicy.Always в продакшене
  • включайте lockout (блокировка после ошибок) и разумные требования к паролю
  • не выводите “пользователь не найден” отдельно от “неверный пароль” в публичных формах входа
  • используйте 2FA для административных аккаунтов
  • Итог

    Теперь у вас есть рабочая система аутентификации для приложения на ASP.NET Core (.NET 10):

  • подключён ASP.NET Core Identity и EF Core хранилище
  • настроена cookies-аутентификация и порядок middleware
  • доступны login/logout и страницы управления аккаунтом
  • вы понимаете, как включается и работает 2FA
  • Следующий логичный шаг курса: авторизация. Мы научимся ограничивать доступ к страницам и действиям через [Authorize], роли и политики, а также корректно показывать 401/403 и интегрировать это в UI.

    6. Авторизация: роли, политики, claims, доступ к ресурсам и админка

    Авторизация: роли, политики, claims, доступ к ресурсам и админка

    В прошлой статье вы подключили аутентификацию через ASP.NET Core Identity: пользователь может войти, получить cookies-сессию, включить 2FA. Но аутентификация отвечает только на вопрос кто ты?.

    Авторизация отвечает на вопрос что тебе можно?. В этой статье вы построите целостную модель доступа для MVC и Razor Pages:

  • роли (Admin, Manager, User)
  • claims (утверждения) и требования к ним
  • политики (policy-based authorization)
  • доступ к конкретным ресурсам (resource-based authorization)
  • базовую админку и UX для 401/403
  • Официальные ссылки:

  • Авторизация в ASP.NET Core
  • Роли в ASP.NET Core Identity
  • Политики авторизации
  • Авторизация на уровне ресурсов
  • Создание пользовательских требований (AuthorizationHandler)
  • Ментальная модель: где живёт авторизация

    После логина у вас появляется HttpContext.User с набором claims (например, идентификатор пользователя, email, роли). Дальше авторизация сравнивает текущего пользователя и требования (например, роль Admin или claim department=Sales) и принимает решение: разрешить доступ или запретить.

    !Где в конвейере выполняются аутентификация и авторизация

    Критично:

  • app.UseAuthentication() должен быть до app.UseAuthorization()
  • endpoint’ы (MapControllerRoute, MapRazorPages) должны быть после middleware авторизации
  • Базовые инструменты: [Authorize], [AllowAnonymous], 401 и 403

    Атрибут [Authorize]

    Самый быстрый способ закрыть страницу или action:

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

  • неаутентифицированный пользователь получит challenge и обычно будет перенаправлен на страницу логина
  • аутентифицированный, но не подходящий по правилам (например, не та роль) получит forbid и попадёт на страницу AccessDenied
  • Атрибут [AllowAnonymous]

    Если вы закрыли весь контроллер/папку, но хотите открыть отдельный endpoint:

    Разница между 401 и 403

  • 401 Unauthorized в веб-терминах означает: вы не вошли (нет аутентификации). В cookie-сценарии это обычно приводит к редиректу на /Identity/Account/Login.
  • 403 Forbidden означает: вы вошли, но доступ запрещён. Это обычно приводит к редиректу на /Identity/Account/AccessDenied.
  • Практический вывод для UX:

  • 401 решается логином
  • 403 решается правами (роль, политика) и должна иметь понятное сообщение
  • Роли: самый понятный уровень доступа

    Роли — хороший старт для админки и простых правил:

  • Admin может всё
  • Manager управляет каталогом
  • User просто пользуется
  • Как включить роли в Identity

    Если вы создавали проект без ролей, подключите роли при регистрации Identity:

    Если вы уже используете AddDefaultIdentity, то для ролей обычно переходят на AddIdentity (как выше), потому что роли требуют RoleManager и IdentityRole.

    Создание ролей и назначение ролей пользователю

    Роли нужно создать один раз (обычно при старте приложения или миграции). Для учебного проекта удобно сделать маленький seeding при старте.

    Пример: создать роль Admin и назначить её пользователю по email из конфигурации.

    appsettings.Development.json:

    Program.cs (после var app = builder.Build();, перед app.Run();):

    Важно:

  • роль назначается существующему пользователю
  • в production обычно делают отдельный процесс инициализации, а не автоматическую раздачу прав при старте
  • Защита контроллеров и страниц по роли

    MVC:

    Razor Pages:

    Claims: более гибкие “права”, чем роли

    Claim — это утверждение о пользователе вида ключ-значение (например, department=Sales, permission=Catalog.Edit). Claims живут в User.Claims.

    Чем claims полезны:

  • роли “грубые” и быстро разрастаются
  • claims удобны для тонких разрешений и интеграций (например, пришли из внешнего провайдера)
  • Проверка claim в политике

    Вместо Roles = ... вы можете потребовать конкретный claim.

    Регистрация политики:

    Использование:

    MVC:

    Razor Pages:

    Откуда брать claims

    Варианты источников:

  • вы добавляете claims при создании пользователя через UserManager.AddClaimAsync
  • вы добавляете claims при логине через IUserClaimsPrincipalFactory или IClaimsTransformation
  • claims приходят от внешнего провайдера (OIDC, OAuth)
  • Для учебного сценария проще всего добавить claim конкретному пользователю вручную (например, в seeding).

    Пример: добавить claim permission=Catalog.Manage администратору.

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

    Политика — это имя правила, которое вы используете в [Authorize(Policy = "...")]. Политики хороши тем, что:

  • правило описано один раз в Program.cs
  • дальше правило применяется во многих местах
  • вы меняете правило централизованно
  • Смешивание ролей и claims в одной политике

    Пример: доступ к управлению каталогом есть у Admin или у пользователя с claim.

    Доступ к ресурсу: когда важен владелец, а не роль

    Роли и policies по endpoint’у отвечают на вопрос можно ли этому пользователю вообще заходить сюда?.

    Но часто нужно другое: можно ли этому пользователю работать с конкретной записью? Например:

  • пользователь может редактировать только свои заказы
  • менеджер может редактировать заказы только в своём регионе
  • Это называется resource-based authorization.

    Идея resource-based подхода

  • Вы загружаете ресурс из базы (например, Order).
  • Вы спрашиваете IAuthorizationService, можно ли выполнить операцию над этим ресурсом.
  • Если нельзя — возвращаете 403.
  • Пример: заказ принадлежит пользователю

    Допустим, в таблице заказов есть OwnerUserId.

    Требование:

    Handler:

    Регистрация:

    Использование в MVC action (важно: ресурс уже загружен):

    Это ключевой паттерн: сначала загрузить ресурс, потом проверить доступ к ресурсу.

    Авторизация в Razor Pages на уровне папок

    Для Razor Pages удобно закрывать целые разделы.

    Пример: закрыть всё в /Pages/Admin только для роли Admin.

    Если нужно открыть одну страницу внутри папки:

    Админка: минимальный каркас и интеграция с UI

    Минимальная “админка” для учебного проекта обычно включает:

  • раздел /Admin
  • страницу со списком пользователей
  • кнопку назначить/снять роль Admin
  • Базовый AdminController (MVC)

    Примечание: UserManager.Users возвращает IQueryable, и для реального проекта вы обычно делаете отдельный слой запросов и явные проекции. Но для учебной админки этого достаточно.

    Скрыть/показать ссылку на админку в navbar

    В layout логика обычно такая:

  • если User.Identity.IsAuthenticated показываем профиль/выход
  • если User.IsInRole("Admin") показываем ссылку Admin
  • Это важно для UX, но помните: скрытие ссылки не является защитой. Реальная защита — это [Authorize] и политики.

    AccessDenied и единый UX для запретов

    Identity по умолчанию использует страницу /Identity/Account/AccessDenied. Вам важно, чтобы:

  • пользователь понимал, что он вошёл, но прав нет
  • в интерфейсе была ссылка “сменить аккаунт” или “вернуться назад”
  • Для предсказуемого поведения настройте cookie-пути:

    Практические правила безопасности

  • Не делайте админские действия через GET (например, “выдать роль”) — используйте POST и антифорджери.
  • Нельзя полагаться на UI-ограничения. Всегда защищайте endpoint’ы через [Authorize] или проверки IAuthorizationService.
  • Роли — хороший старт, но для сложных систем чаще переходят к политикам и permission-claims.
  • Для операций над конкретными записями (владелец/организация) используйте resource-based авторизацию.
  • Итог

    Теперь ваш проект после аутентификации получил полноценную модель контроля доступа:

  • вы понимаете 401 и 403 и настраиваете UX вокруг них
  • используете роли для админки и крупных сценариев
  • используете claims и политики для гибких правил
  • применяете resource-based авторизацию для проверки доступа к конкретной записи
  • умеете защищать MVC actions и Razor Pages, включая целые папки
  • Дальше этот фундамент легко расширяется: роли и policies можно связать с таблицами разрешений, добавить аудит действий, а также встроить административный интерфейс управления пользователями и правами.

    7. Продакшен: логирование, обработка ошибок, безопасность, деплой и тесты

    Продакшен: логирование, обработка ошибок, безопасность, деплой и тесты

    Вы уже умеете собирать ASP.NET Core приложение на .NET 10, строить UI на MVC или Razor Pages, работать с EF Core и миграциями, подключать Identity, а также ограничивать доступ через роли и политики. Следующий шаг профессиональной разработки: сделать приложение предсказуемым в продакшене.

    В продакшене вас интересуют пять вещей:

  • Наблюдаемость: вы понимаете, что происходит (логи, корреляция запросов, метрики).
  • Ошибки: пользователи видят аккуратные страницы, а разработчики получают детали в логах.
  • Безопасность: куки, CSRF, заголовки, управление секретами, защита от типовых атак.
  • Деплой: приложение можно собрать, настроить и запустить одинаково в разных окружениях.
  • Тесты: вы защищаете бизнес-логику и критичные сценарии (логин, доступ, CRUD) от регрессий.
  • !Общая картина того, где в продакшене возникают ошибки и где их правильно логировать

    Полезные официальные ссылки:

  • Logging в ASP.NET Core
  • Обработка ошибок в ASP.NET Core
  • Security в ASP.NET Core
  • Anti-forgery в ASP.NET Core
  • Data Protection в ASP.NET Core
  • Health checks в ASP.NET Core
  • Интеграционные тесты в ASP.NET Core
  • Окружения и конфигурация в продакшене

    ASP.NET Core почти всегда живёт в нескольких окружениях:

  • Development: подробные ошибки, удобство разработки.
  • Staging: максимально похоже на Production, но безопасно для проверок.
  • Production: минимальная утечка деталей, усиленная безопасность.
  • Ключевые практики:

  • Конфигурация должна переопределяться через appsettings.Production.json и переменные окружения.
  • Секреты не хранятся в репозитории.
  • Логирование настраивается конфигурацией, а не правками кода.
  • Минимальный пример чтения строки подключения в продакшене через переменную окружения:

    В приложении это останется тем же кодом:

    Логирование: что логировать и как не утонуть в логах

    Зачем логирование, если есть отладчик

    В продакшене отладчика нет. Логирование отвечает на вопросы:

  • Что именно произошло перед ошибкой.
  • Какие параметры запроса были важны.
  • Почему доступ запрещён (401 или 403).
  • Что медленно работает (в связке с метриками и трассировкой).
  • Базовая модель: ILogger и уровни

    В ASP.NET Core стандартный способ логировать это ILogger<T>.

    Уровни логов, которые полезно различать:

  • Trace и Debug: подробности для разработки.
  • Information: нормальные бизнес-события.
  • Warning: подозрительно, но не авария.
  • Error: ошибка запроса или операции.
  • Critical: приложение не может продолжать работу корректно.
  • Пример типового логирования в сервисе каталога из прошлых статей:

    Ключевая практика: структурированные сообщения. Вместо конкатенации строк используйте шаблоны и параметры, чтобы лог-система могла фильтровать по полям вроде Sku.

    Корреляция логов одного запроса

    Когда один запрос проходит через несколько слоёв (контроллер, сервис, репозиторий), удобно добавлять scope.

    Смысл: все логи внутри scope будут иметь поле ProductId.

    Настройка логов через конфигурацию

    Типовой подход: в Production логов меньше, в Development больше.

    appsettings.Production.json пример логических уровней:

    Практический совет: включать Microsoft.EntityFrameworkCore на Information можно временно, когда вы расследуете конкретную проблему с запросами.

    Обработка ошибок: правильный UX и правильные логи

    Цели обработки ошибок

  • Пользователь не должен видеть стек-трейсы и технические детали.
  • Разработчик должен видеть детали в логах.
  • Ошибка должна возвращать корректный статус.
  • Exception handling middleware

    В Development обычно используют страницу разработчика, а в Production безопасный обработчик.

    Пример шаблонного кода:

    Если у вас Razor Pages приложение, путь обычно такой: /Error.

    404, 403 и единые страницы статусов

    После добавления авторизации у вас появляются важные статусы:

  • 401: пользователь не вошёл, обычно редирект на логин.
  • 403: пользователь вошёл, но прав нет, редирект на AccessDenied.
  • 404: ресурс не найден.
  • Для 404 часто делают красивую страницу через повторный запуск маршрутизации:

    Дальше вы реализуете HomeController.Status(int code), который возвращает представление для 404 и других кодов.

    !Наглядно показывает, почему 401, 403, 404 и 500 обрабатываются разными механизмами

    Что логировать при ошибках

    Практика для MVC и Razor Pages:

  • Для ожидаемых ошибок ввода используйте ModelState и не делайте LogError.
  • Для ошибок сохранения (например, DbUpdateException) логируйте как Warning или Error в зависимости от важности.
  • Для неожиданных исключений логирование обычно сделает инфраструктура, но полезно добавить контекст вокруг ключевых операций.
  • Безопасность: минимум, который должен быть в любом проекте

    HTTPS и HSTS

  • UseHttpsRedirection() перенаправляет на HTTPS.
  • UseHsts() говорит браузеру, что сайт нужно открывать только по HTTPS.
  • Важно: HSTS обычно включают только не в Development.

    Cookies и Identity в продакшене

    Identity использует cookies. Для продакшена важны флаги безопасности:

    Смысл ключевых настроек:

  • HttpOnly: снижает риск кражи cookie через XSS.
  • SecurePolicy.Always: cookie уходит только по HTTPS.
  • SameSite: снижает риск CSRF в типовых сценариях.
  • CSRF и антифорджери

    CSRF это атака, когда браузер пользователя отправляет запрос на ваш сайт, используя его cookies, но пользователь этого не хотел.

    Базовые правила:

  • Все опасные операции делайте через POST, PUT, DELETE, а не через GET.
  • В MVC включайте [ValidateAntiForgeryToken] на POST actions.
  • В Razor Pages защита обычно включена по умолчанию для форм, но важно не отключать её без причины.
  • Ссылка: Anti-forgery в ASP.NET Core

    Data Protection и стабильность cookie между перезапусками

    Cookies Identity подписываются и шифруются системой Data Protection. Если ключи Data Protection теряются при перезапуске, пользователи массово разлогинятся.

    Сценарии риска:

  • контейнеры, которые пересоздаются
  • несколько реплик приложения за балансировщиком
  • Решение: хранить ключи Data Protection в постоянном хранилище.

    Официальная документация: Data Protection в ASP.NET Core

    Минимизация утечек данных

  • Не логируйте пароли, токены, коды 2FA и содержимое auth cookie.
  • Аккуратно с логированием персональных данных. Логи часто доступны большему кругу людей, чем база.
  • Делайте разные уровни логирования для Development и Production.
  • Rate limiting и защита от перебора паролей

    Для входа особенно важны:

  • lockout в Identity (вы настраивали его в статье про аутентификацию)
  • ограничение частоты запросов к логину
  • В ASP.NET Core есть встроенный rate limiting middleware, который можно включать и применять к конкретным endpoint.

    Документация: Rate limiting middleware в ASP.NET Core

    Деплой: сборка, миграции, хостинг, диагностика

    Публикация приложения

    Стандартный путь для деплоя без контейнеров:

    Практика:

  • В продакшене запускайте ASPNETCORE_ENVIRONMENT=Production.
  • Секреты передавайте через переменные окружения или секрет-хранилища.
  • Reverse proxy и Kestrel

    Частый продакшен-сетап:

  • Nginx или IIS как reverse proxy
  • Kestrel как внутренний веб-сервер приложения
  • Плюсы reverse proxy:

  • централизованный TLS
  • сжатие, кэширование, лимиты
  • удобнее логировать на уровне инфраструктуры
  • Официальный раздел: Host and deploy в ASP.NET Core

    Миграции EF Core в продакшене

    У вас есть два типовых подхода.

  • Миграции в пайплайне деплоя: перед запуском новой версии выполняется dotnet ef database update.
  • Миграции при старте приложения: приложение само применяет миграции при запуске.
  • Для учебных проектов удобен второй подход, но в реальном продакшене чаще выбирают первый, чтобы контролировать изменения схемы.

    Если вы всё же применяете миграции при старте, делайте это осознанно и логируйте:

    Health checks

    Health checks позволяют инфраструктуре понять, живо ли приложение.

    Подключение:

    Практика:

  • /health должен быть быстрым.
  • Для сложных систем делают отдельные проверки: база, внешние сервисы.
  • Документация: Health checks в ASP.NET Core

    Логи в контейнерах

    Если вы деплоите в контейнеры, базовая практика:

  • писать логи в stdout и stderr
  • собирать их внешней системой (например, через платформу оркестрации)
  • В таком режиме вам особенно важны:

  • структурированные логи
  • корректные уровни
  • корреляция по запросу
  • Тесты: как проверять MVC, Razor Pages, Identity и авторизацию

    Тесты в веб-приложении полезно разделять на два слоя:

  • Unit-тесты: проверяют бизнес-логику сервисов без HTTP.
  • Integration-тесты: поднимают приложение в памяти и делают реальные HTTP-запросы.
  • Unit-тесты

    Что обычно тестируют unit-тестами:

  • сервисы, которые реализуют правила (например, расчёты, проверки, выборки)
  • отдельные компоненты валидации
  • Что плохо подходит для unit-тестов:

  • контроллеры, которые сильно зависят от HTTP контекста
  • EF Core запросы, которые важны именно своим SQL-поведением
  • Integration-тесты через WebApplicationFactory

    Integration-тесты в ASP.NET Core обычно делают через WebApplicationFactory, который поднимает ваш Program.cs в тестовом хосте.

    Концептуальный пример:

    Это проверяет, что приложение стартует, маршрутизация работает, и главная страница отвечает.

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

    Тестирование авторизации

    Для тестов доступа вам нужно проверять минимум два сценария:

  • анонимный пользователь получает редирект на логин (или 401)
  • пользователь без прав получает 403 (или редирект на AccessDenied)
  • В интеграционных тестах обычно делают тестовую схему аутентификации или подменяют пользователя на фейкового через тестовый хост. Это лучше, чем пытаться логиниться через реальный UI в каждом тесте.

    Тестирование EF Core

    Для тестов с EF Core есть практичные варианты:

  • SQLite in-memory, чтобы поведение было ближе к реальной базе
  • отдельная тестовая база с миграциями, если вы хотите максимальную реалистичность
  • Важно: InMemory provider EF Core часто ведёт себя не так, как настоящая реляционная база, и может скрывать проблемы.

    Итоговый чеклист перед продакшеном

  • Ошибки
  • - UseExceptionHandler включён для Production. - Есть страница для 404 и понятный UX для 403.
  • Логи
  • - Уровни логирования заданы для Production. - Нет утечки секретов в логах. - Есть корреляция или scope для важных операций.
  • Безопасность
  • - HTTPS и HSTS включены правильно. - Cookie настроены безопасно. - CSRF защита не отключена. - Data Protection ключи не теряются при рестартах и масштабировании.
  • Деплой
  • - Конфигурация и секреты приходят извне. - Понятно, как применяются миграции. - Есть health endpoint.
  • Тесты
  • - Есть минимальные integration smoke-тесты. - Проверены сценарии доступа: анонимный, без прав, с правами.

    На этом этапе ваш проект становится не просто учебным, а близким к реальным требованиям: его можно сопровождать, диагностировать и безопасно развивать.