Uploaded by udinds

aspnet-core-aspnetcore-7.0

advertisement
Оставьте отзыв о скачивании PDF-файла.
Документация по ASP.NET
Узнайте, как с помощью ASP.NET Core создавать быстрые и безопасные кроссплатформенные
или облачные веб-приложения и службы. Изучайте руководства, примеры кода, основные
понятия, справочник по API и многое другое.
НАЧАЛО РАБОТЫ
ОБЩИЕ СВЕДЕНИЯ
СКАЧАТЬ
НОВОЕ
НАЧАЛО РАБОТЫ
НАЧАЛО РАБОТЫ
НАЧАЛО РАБОТЫ
ОБЩИЕ СВЕДЕНИЯ
Создание приложения
ASP.NET Core на любой
платформе за 5 минут
Скачать .NET
Обзор ASP.NET Core
Новые возможности в
документации по ASP.NET Core
Создание первого
пользовательского вебинтерфейса
Создание первого вебприложения в реальном
времени
Создание первого веб-API
Документация по ASP.NET 4.x
Разработка приложений ASP.NET Core
Выбирайте между интерактивными веб-приложениями, веб-API, приложениями на базе
шаблонов MVC, приложениями реального времени и многим другим.
Интерактивные
приложения Blazor на
Приложения API HTTP
Разработка служб HTTP с
Ориентированный на
страницы веб-
стороне клиента
помощью ASP.NET Core
интерфейс с…
Применяйте в разработке
взаимозаменяемые
компоненты
пользовательского…
b Создание минимального
Разрабатывайте основанные
на страницах вебприложения с четким
разделением…
e Обзор
b Создание приложения
Blazor
b Создание первого
приложения Blazor с
взаимозаменяемыми
компонентами
p Модели размещения
Blazor
веб-API с помощью
ASP.NET Core
d Созданию веб-API с
помощью контроллеров
ASP.NET Core
g Создание страниц
справки по веб-API с
использованием
Swagger/OpenAPI
p Типы возвращаемых
значений действий
контроллера
p Форматирование данных
ответа
p Обработка ошибок
g Вызов веб-API
b Создание первого веб-
приложения Razor Pages
g Создание страничного
пользовательского вебинтерфейса с
применением веб-API
p Синтаксис Razor
p Фильтры
p Маршрутизация
p Доступные веб-
приложения ASP.NET
Core
ASP.NET Core с помощью
JavaScript
Ориентированный на
страницы вебинтерфейс с MVC
Веб-приложения
реального времени
с SignalR
Приложения
удаленного вызова
процедур (RPC) —…
Разрабатывайте вебприложения с помощью
шаблона проектирования
MVC (модель —…
Добавьте в свое вебприложение функции
реального времени и
используйте код на стороне…
Разрабатывайте
высокопроизводительные
службы на базе модели
"сначала контракт" с…
e Обзор
e Обзор
e Обзор
b Создание первого веб-
b Создание первого
b Создание клиента и
g SignalR с Blazor
p Основные понятия служб
g Использование SignalR с
s Примеры
приложения MVC в
ASP.NET Core
p Представления
p Частичные представления
p Контроллеры
приложения SignalR
WebAssembly
TypeScript
p Маршрутизация к
s Примеры
p Модульный тест
p Функции клиентов SignalR
действиям контроллера
p Концентраторы
p Размещение и
масштабирование
сервера gRPC
gRPC в C#
p Сравнение служб gRPC с
API-интерфейсами HTTP
g Добавление службы gRPC
в приложение
ASP.NET Core
g Вызов служб gRPC с
помощью клиента .NET
g Использование gRPC в
приложениях на основе
браузера
Веб-приложения на
основе данных
Предыдущие версии
платформы ASP.NET
Создавайте веб-приложения,
работающие на основе
данных, в ASP.NET Core.
Изучайте обзоры, учебники,
основные понятия,
архитектуру и справочники
по API для предыдущих…
g SQL в ASP.NET Core
p Привязка данных к Blazor
p ASP.NET 4.x
в ASP.NET Core
Учебные видео по
ASP.NET Core
q Серия видео об ASP.NET
Core 101
q Серия видео об Entity
Framework Core 101, .NET
Core и ASP.NET Core
q Архитектура микрослужб
и ASP.NET Core
g SQL Server Express и
Razor Pages
q Серия видео о Blazor
g Entity Framework Core и
q .NET Channel
Razor Pages
g Entity Framework Core и
MVC в ASP.NET Core
g Хранилище Azure
g Хранилище BLOBобъектов
p Хранилище таблиц Azure
p Сценарии Microsoft Graph
для ASP.NET Core
Концепции и функции
Справочник по API для ASP.NET Core
Размещение и развертывание
Браузер API .NET
Обзор
Развертывание в службе приложений Azure
DevOps для разработчиков ASP.NET Core
Linux c Apache
Linux с Nginx
Kestrel
IIS
Docker
Безопасность и идентификация
Глобализация и локализация
Обзор
Обзор
Аутентификация
Локализация переносимых объектов
Авторизация
Расширяемость локализации
Курс. Защита веб-приложения ASP.NET Core с
помощью Identity Framework
Устранение неполадок
Защита данных
Управление секретами
Принудительное использование HTTPS
Размещение Docker с использованием HTTPS
Тестирование, отладка и устранение
неполадок
Azure и ASP.NET Core
Модульные тесты Razor Pages
ASP.NET Core и Docker
Удаленная отладка
Отладка моментальных снимков
Размещение веб-приложения в Службе
приложений Azure
Интеграционные тесты
Служба приложений и База данных SQL Azure
Нагрузочное тестирование
Управляемое удостоверение для ASP.NET Core и
Базы данных SQL Azure
Устранение неполадок и отладка
Развертывание веб-приложения ASP.NET Core
Ведение журнала
Веб-API с поддержкой CORS в Службе
приложений Azure
Нагрузочное тестирование веб-приложений
Azure с помощью Azure DevOps
Сбор журналов веб-приложений с помощью
журналов диагностики Службы приложений
Производительность
Дополнительные функции
Обзор
Привязка модели
Память и сборка мусора
Проверка модели
Кэширование откликов
Написание ПО промежуточного слоя
Сжатие ответов
Операции запросов и ответов
Диагностические средства
Переопределение URL-адресов
Нагрузочное тестирование
Миграция
Architecture
С версии ASP.NET Core 5.0 на 6.0
Выбор между традиционными вебприложениями и одностраничными
приложениями (SPA)
Примеры кода с версии ASP.NET Core 5.0 на 6.0
для минимальной модели размещения
С версии ASP.NET Core 3.1 на 5.0
С версии ASP.NET Core 3.0 на 3.1
С версии ASP.NET Core 2.2 на 3.0
С версии ASP.NET Core 2.1 на 2.2
Архитектурные принципы
Общие архитектуры веб-приложений
Распространенные клиентские веб-технологии
Процесс разработки для Azure
Переход с ASP.NET Core 2.0 на 2.1
Миграция с ASP.NET Core 1.x на 2.0
Миграция с ASP.NET на ASP.NET Core
Примите участие в создании документации по ASP.NET Core. Ознакомьтесь с нашим руководством для
соавторов .
Документация по ASP.NET Core —
новые возможности
Добро пожаловать в раздел о новых возможностях в документации по ASP.NET
Core. На этой странице вы сможете быстро найти последние изменения.
Поиск обновлений в документации по ASP.NET Core
h
НОВОЕ
Июнь 2022 г.
Март 2022 г.
Февраль 2022 г.
Январь 2022 г.
Декабрь 2021 г.
Ноябрь 2021 г.
Принимайте участие в разработке документации по ASP.NET Core
e
ОБЩИЕ СВЕДЕНИЯ
Репозиторий документов по ASP.NET Core
Структура и метки проекта для проблем и запросов на вытягивание
p
КОНЦЕПЦИЯ
Руководство для соавторов документации Майкрософт
Руководство для соавторов документации по ASP.NET Core
Руководство для соавторов справочной документации по API ASP.NET Core
Сообщество
h
НОВОЕ
Сообщество
Сообщество
Связанные страницы о новых возможностях
h
НОВОЕ
Обновления документации по Xamarin
Заметки о выпуске .NET Core
Заметки о выпуске ASP.NET Core
Заметки о выпуске компилятора C# (Roslyn)
Заметки о выпуске Visual Studio
Заметки о выпуске Visual Studio для Mac
Заметки о выпуске Visual Studio Code
Общие сведения об ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 8 мин
Авторы: Дэниэл Рот (Daniel Roth) , Рик Андерсон (Rick Anderson)
и Шон Луттин
(Shaun Luttin)
ASP.NET Core является кроссплатформенной, высокопроизводительной средой с
открытым исходным кодом
для создания современных облачных приложений,
подключенных к Интернету.
ASP.NET Core позволяет выполнять следующие задачи:
Создавать веб-приложения и службы, приложения Интернета вещей
(IoT) и
серверные части для мобильных приложений.
Использовать избранные средства разработки в Windows, macOS и Linux.
Выполнять развертывания в облаке или локальной среде.
Запускать в .NET Core.
Преимущества, обеспечиваемые ASP.NET
Core
Миллионы разработчиков использовали и продолжают использовать ASP.NET 4.x
для создания веб-приложений. ASP.NET Core — это модификация ASP.NET 4.x с
архитектурными изменениями, формирующими более рациональную и более
модульную платформу.
ASP.NET Core предоставляет следующие преимущества:
Единое решение для создания пользовательского веб-интерфейса и веб-API.
Разработано для тестируемости.
Razor Pages упрощает написание кода для сценариев страниц и повышает его
эффективность.
Blazor позволяет использовать в браузере язык C# вместе с JavaScript.
совместное использование серверной и клиентской логик приложений,
написанных с помощью .NET;
Возможность разработки и запуска в ОС Windows, macOS и Linux.
Открытый исходный код и ориентация на сообщество .
Интеграция современных клиентских платформ и рабочих процессов
разработки.
Поддержка размещения служб удаленного вызова процедур (RPC) с помощью
gRPC.
Облачная система конфигурации на основе среды.
Встроенное введение зависимостей.
Упрощенный высокопроизводительный
модульный конвейер HTTP-
запросов.
Следующие возможности размещения:
Kestrel
Службы IIS
HTTP.sys
Nginx
Apache
Docker
Управление параллельными версиями.
Инструментарий, упрощающий процесс современной веб-разработки.
Создание веб-API и пользовательского вебинтерфейса с помощью ASP.NET Core MVC
ASP.NET Core MVC предоставляет функции, которые позволяют создавать вебинтерфейсы API и веб-приложения.
Шаблон Model-View-Controller (MVC) помогает сделать веб-API и вебприложения тестируемыми.
Razor Pages — это основанная на страницах модель программирования,
которая упрощает и повышает эффективность создания пользовательского
веб-интерфейса.
Разметка Razor предоставляет эффективный синтаксис для страниц Razor
Pages и представлений MVC.
Вспомогательные функции тегов позволяют серверному коду участвовать в
создании и отображении HTML-элементов в файлах Razor.
Благодаря встроенной поддержке нескольких форматов данных и
согласованию содержимого веб-API становятся доступными для множества
клиентов, включая браузеры и мобильные устройства.
Привязка модели автоматически сопоставляет данные из HTTP-запросов с
параметрами методов действия.
Проверка модели автоматически выполняется на стороне сервера и клиента.
Клиентская разработка
ASP.NET Core легко интегрируется с распространенными клиентскими
платформами и библиотеками, в том числе Blazor, Angular, React и Bootstrap .
Подробнее см. в статье ASP .NET CoreBlazor и сопутствующих материалах в разделе
Разработка на стороне клиента.
Целевые версии платформы ASP.NET Core
ASP.NET Core 3.x или более поздней версии можно использовать только для .NET
Core. Как правило, ASP.NET Core состоит из библиотек .NET Standard. Библиотеки,
написанные на .NET Standard 2.0 под управлением любой платформы .NET с
реализацией .NET Standard 2.0.
При использовании .NET Core существуют некоторые преимущества, и их число
увеличивается с каждым выпуском. Преимущества .NET Core по сравнению с
.NET Framework включают:
Кроссплатформенность. Выполняется в Windows, macOS и Linux.
Повышение производительности
Управление параллельными версиями
Новые интерфейсы API
Открытый код
Рекомендуемая схема обучения
Для знакомства с разработкой приложений ASP.NET Core рекомендуется изучить
следующую последовательность учебников.
1. Пройдите учебник по тому типу приложения, которое вы собираетесь
разрабатывать или обслуживать.
Тип приложения
Сценарий
Учебник
Веб-приложение
Разработка нового веб-интерфейса на
стороне сервера
Начало работы
с Razor Pages
Веб-приложение
Обслуживание приложения MVC
Начало работы
с MVC
Веб-приложение
Разработка веб-интерфейса на стороне
клиента
Начало работы
с Blazor
Веб-интерфейс API
Службы HTTP RESTful
Создание вебAPI†
Тип приложения
Сценарий
Учебник
Приложение
удаленного вызова
процедур
Разработка в соответствии с парадигмой
"Сначала контракт" с использованием Protocol
Buffers
Начало работы
со
службой gRPC
Приложение
Двунаправленный обмен данными между
Начало работы
режима реального
времени
сервером и подключенными к нему
клиентами
с SignalR
2. Пройдите учебник, посвященный основам доступа к данным.
Сценарий
Учебник
Разработка нового приложения
Razor Pages с Entity Framework Core
Обслуживание приложения MVC
MVC с Entity Framework Core
3. Ознакомьтесь с обзором основ ASP.NET Core, относящихся ко всем типам
приложений.
4. Просмотрите содержание, чтобы найти другие интересующие вас темы.
†Доступен интерактивный учебник по веб-API. Локальная установка средств
разработки не требуется. Код выполняется в Azure Cloud Shell
в браузере, а для
тестирования используется curl .
Миграция с .NET Framework
Справочное руководство по миграции приложений ASP.NET 4.х на ASP.NET Core см.
в статье Миграция с ASP.NET на ASP.NET Core.
Загрузка примера
Многие статьи и учебники содержат ссылки на примеры кода.
1. Загрузите ZIP-файл репозитория ASP.NET .
2. Распакуйте файл AspNetCore.Docs-main.zip .
3. Чтобы получить доступ к примеру приложения из статьи в распакованном
репозитории, используйте URL-адрес примера ссылки из статьи для перехода
к папке примера. Как правило, пример ссылки из статьи отображается в ее
верхней части. Текст ссылки: Просмотрите или загрузите пример кода.
Директивы препроцессора в примере кода
Для демонстрации нескольких сценариев в примерах приложений используются
директивы препроцессора #define и #if-#else/#elif-#endif , выборочно
компилирующие и запускающие разные фрагменты примеров кода. В примерах,
где применяется этот подход, задайте в начале файлов C# директиву #define для
определения символа, связанного со сценарием, который нужно запустить. Для
запуска сценария в некоторых примерах потребуется определить символ в начале
нескольких файлов.
Например, в следующем списке символов #define видно, что доступно четыре
сценария (один сценарий на символ). В текущем примере конфигурации
запускается сценарий TemplateCode :
C#
#define TemplateCode // or LogFromMain or ExpandDefault or FilterInCode
Чтобы запустить в примере сценарий ExpandDefault , задайте символ ExpandDefault
и оставьте остальные символы раскомментированными:
C#
#define ExpandDefault // TemplateCode or LogFromMain or FilterInCode
Дополнительные сведения об использовании директив препроцессора C# для
выборочной компиляции фрагментов кода см. в разделах #define (Справочник по
C#) и #if (Справочник по C#).
Регионы в примере кода
Некоторые примеры приложений содержат фрагменты кода внутри директив C#
#region и #endregion. Система сборки документации вставляет эти регионы в
обработанные разделы документации.
Имена регионов обычно содержат слово "фрагмент". В следующем примере
показан регион с именем snippet_WebHostDefaults :
C#
#region snippet_WebHostDefaults
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
#endregion
На предыдущий фрагмент кода C# указывает ссылка в следующей строке в файле
Markdown раздела:
Markdown
[!code-csharp[](sample/SampleApp/Program.cs?name=snippet_WebHostDefaults)]
Вы можете спокойно проигнорировать или удалить директивы #region и
#endregion вокруг кода. Не изменяйте код внутри этих директив, если планируете
запустить примеры сценариев, описанные в разделе. Вы можете изменить код,
экспериментируя с другими сценариями.
Дополнительные сведения см. в разделеУчастие в написании документации
ASP.NET: Фрагменты кода .
Критические изменения и советы по
безопасности
Критические изменения и рекомендации по безопасности отображаются в
репозитории объявлений . Объявления можно ограничить определенной
версией, выбрав фильтр меток.
Дальнейшие действия
Дополнительные сведения см. в следующих ресурсах:
Начиная работать с ASP.NET Core
Публикация приложения ASP.NET Core в Azure с помощью Visual Studio
Основы ASP.NET Core
В еженедельном выпуске ASP.NET Community Standup
рассматривается ход
работы и планы команды. Помимо этого, публикуются новые блоги и
стороннее программное обеспечение.
Выбор между ASP.NET 4.x и ASP.NET
Core
Статья • 28.01.2023 • Чтение занимает 2 мин
ASP.NET Core является переработанной версией ASP.NET 4.x. В этой статье
перечислены различия между ними.
ASP.NET Core
ASP.NET Core — это кроссплатформенная среда с открытым кодом для создания
современных облачных веб-приложений в Windows, macOS или Linux.
ASP.NET Core предоставляет следующие преимущества:
Единое решение для создания пользовательского веб-интерфейса и веб-API.
Разработано для тестируемости.
Razor Pages упрощает написание кода для сценариев страниц и повышает его
эффективность.
Blazor позволяет использовать в браузере язык C# вместе с JavaScript.
совместное использование серверной и клиентской логик приложений,
написанных с помощью .NET;
Возможность разработки и запуска в ОС Windows, macOS и Linux.
Открытый исходный код и ориентация на сообщество .
Интеграция современных клиентских платформ и рабочих процессов
разработки.
Поддержка размещения служб удаленного вызова процедур (RPC) с помощью
gRPC.
Облачная система конфигурации на основе среды.
Встроенное введение зависимостей.
Упрощенный высокопроизводительный
запросов.
Следующие возможности размещения:
Kestrel
Службы IIS
HTTP.sys
Nginx
Apache
Docker
Управление параллельными версиями.
модульный конвейер HTTP-
Инструментарий, упрощающий процесс современной веб-разработки.
ASP.NET 4.x
ASP.NET 4.x — это развитая платформа, предоставляющая необходимые службы
для создания серверных веб-приложений корпоративного класса в Windows.
Выбор платформы
В следующей таблице сравниваются ASP.NET Core и ASP.NET 4.x.
ASP.NET Core
ASP.NET 4.x
Предназначена для Windows, macOS или Linux
Предназначена для Windows
Razor Pages — рекомендуемый метод создания
Использование веб-форм,
пользовательского веб-интерфейса в ASP.NET Core 2.x.
См. также сведения об MVC, веб-API и SignalR.
SignalR, MVC, веб-API, вебперехватчиков или веб-страниц
Несколько версий для одного компьютера
Одна версия для одного
компьютера
Разработка в Visual Studio , Visual Studio для Mac
Visual Studio Code с использованием C# или F#
или
Разработка с Visual Studio с
использованием C#, VB или F#
Более высокая производительность, чем в ASP.NET 4.x
Хорошая производительность
Использование среды выполнения .NET Core
Использование среды
выполнения .NET Framework
Дополнительные сведения о поддержке ASP.NET Core 2.x на платформе .NET
Framework см. в разделе ASP.NET Core для платформы .NET Framework.
Сценарии ASP.NET Core
Веб-сайты
API-интерфейсы
Режим реального времени
Развертывание приложения ASP.NET Core в Azure
Сценарии ASP.NET 4.x
Веб-сайты
API-интерфейсы
Режим реального времени
Создание веб-приложение ASP.NET 4.x в Azure
Дополнительные ресурсы
Введение в ASP.NET
Введение в ASP.NET Core
Развертывание приложений ASP.NET Core в Службе приложений Azure
.NET и .NET Framework для серверных
приложений
Статья • 05.10.2022 • Чтение занимает 5 мин
Для создания серверных приложений доступны две поддерживаемые реализации
.NET.
Реализация
Включенные версии
.NET
.NET Core 1.0–3.1, .NET 5 и более поздние версии .NET.
.NET Framework
.NET Framework 1.0–4.8
В них используется множество одинаковых компонентов, а код можно
использовать как в одной среде, так и в другой. Однако между ними есть
фундаментальные различия, и ваш выбор зависит от того, что вы хотите достичь. В
этой статье содержатся рекомендации по использованию каждой из двух сред
выполнения.
Используйте среду .NET для создания серверных приложений в следующих случаях:
для создания кроссплатформенных решений;
для создания решений, ориентированных на микрослужбы;
при использовании контейнеров Docker;
если нужны масштабируемые системы с высокой производительностью;
для создания приложений с поддержкой разных версий .NET.
Используйте среду .NET Framework для создания серверных приложений в
следующих случаях:
приложение в настоящий момент использует среду .NET Framework (мы
рекомендуем расширять приложения, а не переносить их в другую среду);
приложение использует сторонние библиотеки или пакеты NuGet,
недоступные для .NET;
приложение использует технологии .NET Framework, недоступные для .NET;
приложение использует платформу, не поддерживающую .NET.
Случаи использования .NET
В следующих разделах более подробно описаны ранее перечисленные причины
для выбора платформы .NET вместо .NET Framework.
Создание кроссплатформенных приложений
Если веб-приложение или служба будут работать на нескольких платформах
(Windows, Linux и macOS), используйте платформу .NET.
В среде .NET также можно использовать упомянутые ранее операционные системы
в качестве рабочих станций для разработки. Visual Studio предоставляет
интегрированную среду разработки (IDE) для Windows и macOS. Можно также
использовать редактор Visual Studio Code, который выполняется на платформах
macOS, Linux и Windows. Visual Studio Code поддерживает .NET, включая
технологию IntelliSense и отладку. С .NET работает большинство сторонних
редакторов, например Sublime, Emacs и VI. Эти сторонние редакторы получают
доступ к функциям в редакторе IntelliSense с помощью Omnisharp . Вы также
можете избежать любого редактора кода и напрямую использовать .NET CLI,
который доступен для всех поддерживаемых платформ.
Архитектура микрослужб
Архитектура микрослужб позволяет использовать сочетание технологий за
пределами службы. Такое сочетание технологий позволяет постепенно добавлять
новые микрослужбы в .NET для параллельного использования с другими службами
и микрослужбами. Например, можно комбинировать микрослужбы или службы,
созданные на основе .NET Framework, Java, Ruby или других монолитные
технологий.
Пользователям на выбор предоставляется множество инфраструктурных платформ.
Для больших и сложных систем микрослужб можно использовать Azure Service
Fabric
. Служба приложений Azure
лучше всего подойдет для микрослужб без
сохранения состояния. Альтернативы микрослужб на основе Docker подходят к
любым микрослужбам, как описано в разделе "Контейнеры ". Все эти платформы
поддерживают .NET и идеально подходят для размещения микрослужб.
Дополнительные сведения об архитектуре микрослужб см. в статье Микрослужбы
.NET: архитектура контейнерных приложений .NET.
Контейнеры
Контейнеры обычно используются с архитектурой микрослужб. Их также можно
использовать, чтобы поместить в контейнер веб-приложения или службы на базе
любого архитектурного шаблона. платформа .NET Framework можно использовать
в контейнерах Windows. Тем не менее, модульность и упрощенный характер .NET
делают его лучшим выбором для контейнеров. При создании и развертывании
контейнера размер его образа намного меньше в .NET, чем при использовании
платформа .NET Framework. Так как она кроссплатформенная, вы можете
развертывать серверные приложения в контейнерах Docker для Linux.
Контейнеры Docker можно размещать в собственной инфраструктуре Linux или
Windows или в облачной службе, например в Служба Azure Kubernetes . Служба
Azure Kubernetes может выполнять оркестрацию и масштабировать приложения на
основе контейнеров, а также управлять ими в облаке.
Масштабируемые системы с высокой
производительностью
Если для вашей системы требуется максимальная производительность и
возможности масштабирования, мы рекомендуем использовать среды .NET и
ASP.NET Core. Высокопроизводительная среда выполнения сервера для Windows
Server и Linux делает ASP.NET Core высокопроизводительной веб-платформы на
тестах TechEmpower .
Производительность и масштабируемость особенно важны для архитектур
микрослужб, где могут работать сотни микрослужб. Среда ASP.NET Core позволяет
уменьшить количество серверов и виртуальных машин, необходимых для системы.
Сокращенные серверы и виртуальные машины экономят затраты на
инфраструктуру и размещение.
Параллельные версии .NET на уровне приложения
Если требуется установить приложения с зависимостями в разных версиях
платформ .NET, рекомендуется использовать среду .NET. Эта реализация
поддерживает параллельную установку разных версий среды выполнения .NET на
одном компьютере. Параллельная установка позволяет использовать несколько
служб на одном сервере, каждая из которых имеет собственную версию .NET. Это
позволяет устранить риски и сократить расходы на обновление приложений и ИТоперации.
Параллельная установка невозможна при использовании .NET Framework. Это
компонент Windows, и на компьютере может существовать только одна версия
этого компонента. Каждая версия .NET Framework заменяет предыдущую версию.
Если вы устанавливаете новое приложение, предназначенное для более поздней
версии платформа .NET Framework, вы можете нарушить существующие
приложения, которые выполняются на компьютере, так как предыдущая версия
была заменена.
Случаи использования .NET Framework
Среда .NET предоставляет значительные преимущества для новых приложений и
шаблонов приложений. Но платформа .NET Framework остается оптимальным
выбором во многих ситуациях, поэтому .NET не заменит .NET Framework для всех
серверных приложений.
Готовые приложения .NET Framework
В большинстве случаев вам не потребуется переносить готовые приложения в
среду .NET. Вместо этого рекомендуется использовать .NET при расширении
существующего приложения, например при написании новой веб-службы в
ASP.NET Core.
Сторонние библиотеки .NET и пакеты NuGet,
недоступные для .NET
.NET Standard позволяет совместно использовать код во всех реализациях .NET,
включая .NET Core/5+. В .NET Standard 2.0 этот режим совместимости позволяет
проектам .NET Standard и .NET ссылаться на библиотеки .NET Framework.
Дополнительные сведения см. в статье Поддержка библиотек платформы .NET
Framework.
Платформу .NET Framework следует применять только в случаях, где библиотеки
или пакеты NuGet используют технологии, которые недоступны в .NET Standard и
.NET.
Технологии .NET Framework, недоступные в .NET
Некоторые технологии .NET Framework недоступны в среде .NET. Ниже приведен
список самых распространенных технологий, которые недоступны в .NET:
ASP.NET Web Forms приложения: ASP.NET Web Forms доступны только в
платформа .NET Framework. ASP.NET Core нельзя использовать для ASP.NET
Web Forms.
веб-страницы ASP.NET приложения: веб-страницы ASP.NET не включены в
ASP.NET Core.
Службы, связанные с рабочими процессами: Windows Workflow Foundation
(WF), службы рабочих процессов (WCF + WF в одной службе) и WCF Data
Services (прежнее название — "службы данных ADO.NET") доступны только в
платформа .NET Framework.
Поддержка языка: Visual Basic и F# в настоящее время поддерживаются в
.NET, но не для всех типов проектов. Список поддерживаемых шаблонов
проектов см. в статье о параметрах шаблона для dotnet new.
Дополнительные сведения см. в статье Технологии .NET Framework, недоступные в
.NET Core и .NET 5 и более поздних версий.
Платформа не поддерживает .NET
Некоторые платформы Майкрософт и платформы сторонних поставщиков не
поддерживают среду .NET. Некоторые службы Azure предоставляют пакеты SDK,
недоступные в среде .NET. В таких случаях в качестве альтернативы клиентскому
пакету SDK можно использовать REST API.
См. также
Выбор между ASP.NET и ASP.NET Core
ASP.NET Core с целевой платформой .NET Framework
Целевые платформы
Общие сведения о платформе .NET
Перенос кода в .NET 5 из .NET Framework
Общие сведения о .NET и Docker
Реализации .NET
Микрослужбы .NET. Архитектура контейнерных приложений .NET
Учебник. Начало работы с ASP.NET
Core
Статья • 28.01.2023 • Чтение занимает 2 мин
В этом руководстве показано, как с помощью .NET Core CLI создать и запустить вебприложение ASP.NET Core.
Вы научитесь:
" создавать проект веб-приложения;
" устанавливать доверие к сертификату разработки;
" Запустите приложение.
" Измените страницу Razor.
В итоге вы получите рабочее веб-приложение на локальном компьютере.
Предварительные требования
Пакет SDK для .NET 6.0
Создание проекта веб-приложения
Откройте окно командной оболочки и введите следующую команду:
Интерфейс командной строки.NET
dotnet new webapp -o aspnetcoreapp
Предыдущая команда позволяет:
создать веб-сайт;
с помощью параметра -o aspnetcoreapp создать каталог aspnetcoreapp с
исходными файлами приложения.
Установка доверия к сертификату разработки
Установите доверие к сертификату разработки HTTPS.
Windows
Интерфейс командной строки.NET
dotnet dev-certs https --trust
Приведенная выше команда отображает следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Дополнительные сведения см. в разделе Trust the ASP.NET Core HTTPS development
certificate (Настройка доверия к сертификату разработки HTTPS ASP.NET Core).
Запуск приложения
Выполните следующие команды:
Интерфейс командной строки.NET
cd aspnetcoreapp
dotnet watch run
Когда в командной оболочке будет показано, что приложение запущено, откройте
страницу https://localhost:{port} , где {port} — случайный порт.
Изменение страницы Razor
Откройте Pages/Index.cshtml , а затем измените и сохраните страницу, добавив
выделенное исправление:
CSHTML
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Hello, world! The time on the server is @DateTime.Now</p>
</div>
Перейдите на страницу https://localhost:{port} , обновите ее и проверьте,
отобразились ли изменения.
Следующие шаги
В этом руководстве вы узнали, как:
" создавать проект веб-приложения;
" устанавливать доверие к сертификату разработки;
" Запустите проект.
" вносить изменения.
Дополнительные сведения об ASP.NET Core:
Общие сведения об ASP.NET Core
Новые возможности в ASP.NET
Core 7.0
Статья • 28.01.2023 • Чтение занимает 24 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 7.0 со
ссылками на соответствующую документацию.
ПО промежуточного слоя для ограничения
скорости в ASP.NET Core
ПО промежуточного слоя Microsoft.AspNetCore.RateLimiting предоставляет ПО
промежуточного слоя для ограничения скорости. Приложения настраивают
политики ограничения скорости, а затем присоединяют политики к конечным
точкам. Дополнительные сведения см. в статье ПО промежуточного слоя для
ограничения скорости в ASP.NET Core.
Проверка подлинности использует одну
схему в качестве DefaultScheme
В рамках работы по упрощению проверки подлинности, если зарегистрирована
только одна схема проверки подлинности, она автоматически используется в
качестве DefaultScheme и не требуется указывать. Дополнительные сведения см. в
разделе DefaultScheme.
MVC и Razor Pages
Поддержка моделей, допускающих значение NULL, в
представлениях MVC и Razor Pages
Модели страниц или представлений, допускающих значение NULL,
поддерживаются для улучшения работы при проверке состояния NULL в
приложениях ASP.NET Core:
C#
@model Product?
Привязка с помощью IParsable<T>.TryParse в
контроллерах API и MVC
API IParsable<TSelf>.TryParse поддерживает привязку значений параметров
действий контроллера. Дополнительные сведения см. в разделе Привязка с
помощью IParsable<T>.TryParse.
Настройка значения согласия cookie
В версиях ASP.NET Core, предшествующих 7, при проверке согласия cookie
используется значение yes cookie, чтобы указать согласие. Теперь можно указать
значение, представляющее согласие. Например, можно использовать true вместо
yes :
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.ConsentCookieValue = "true";
});
var app = builder.Build();
Дополнительные сведения см. в статье о настройке значения согласия cookie.
Контроллеры API
Привязка параметров с помощью внедрения
зависимостей в контроллерах API
Привязка параметров для действий контроллера API позволяет привязать
параметры с помощью внедрения зависимостей при настройке типа в качестве
службы. Это означает, что больше не требуется явно применять атрибут
[FromServices] к параметру. В следующем коде оба действия возвращают время:
C#
[Route("[controller]")]
[ApiController]
public class MyController : ControllerBase
{
public ActionResult GetWithAttribute([FromServices] IDateTime dateTime)
=> Ok(dateTime.Now);
[Route("noAttribute")]
public ActionResult Get(IDateTime dateTime) => Ok(dateTime.Now);
}
В редких случаях автоматическое внедрение зависимостей может прервать работу
приложения, если оно имеет тип, который также принимается в методах действий
контроллеров API. Обычно тип во внедрении зависимостей и тип, используемый в
качестве аргумента в действии контроллера API, не совпадают. Чтобы отключить
автоматическую привязку параметров, задайте
DisableImplicitFromServicesParameters:
C#
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.DisableImplicitFromServicesParameters = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
В ASP.NET Core 7.0 типы во внедрении зависимостей проверяются при запуске
приложения с помощью IServiceProviderIsService, чтобы определить, поступает ли
аргумент в действии контроллера API из внедрения зависимостей или из других
источников.
Новый механизм вывода источника привязки параметров действия контроллера
API использует следующие правила:
1. Указанный ранее BindingInfo.BindingSource не перезаписывается.
2. Назначается параметр сложного типа, зарегистрированный в контейнере
внедрения зависимостей: BindingSource.Services.
3. Назначается параметр сложного типа, не зарегистрированный в контейнере
внедрения зависимостей: BindingSource.Body.
4. Параметру с именем, которое появляется как значение маршрута в любом
шаблоне маршрута, присваивается BindingSource.Path.
5. В качестве остальных параметров используется BindingSource.Query.
Имена свойств JSON в ошибках проверки
По умолчанию при возникновении ошибки проверки в результате проверки
модели создается ModelStateDictionary с именем свойства в качестве ключа
ошибки. Некоторые приложения, например одностраничные, используют имена
свойств JSON для ошибок проверки, созданных из веб-API. В следующем коде для
проверки настраивается использование SystemTextJsonValidationMetadataProvider
для использования имен свойств JSON:
C#
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
SystemTextJsonValidationMetadataProvider());
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
В следующем коде для проверки настраивается использование
NewtonsoftJsonValidationMetadataProvider для использования имени свойства JSON
при применении Json.NET :
C#
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new
NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Дополнительные сведения см. в разделе Использование имен свойств JSON в
ошибках проверки.
Минимальные API
Фильтры в приложениях с минимальным API
Фильтры минимальных API позволяют разработчикам реализовать бизнес-логику,
которая поддерживает следующее:
Выполнение кода до и после запуска обработчика маршрутов.
Проверка и изменение параметров, предоставленных при вызове
обработчика маршрутов.
Перехват ответа обработчика маршрутов.
Фильтры могут быть полезны в следующих сценариях:
Проверка параметров и текста запросов, отправляемых в конечную точку.
Запись сведений о запросе и ответе в журнал.
Проверка того, что запрос предназначен для поддерживаемой версии API.
Дополнительные сведения см. в разделе Фильтры в минимальных приложениях
API.
Привязка массивов и строковых значений из
заголовков и строк запроса
В ASP.NET 7 поддерживается привязка строк запроса к массиву примитивных
типов, массивам строк и StringValues.
C#
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
Привязка строк запроса или значений заголовков к массиву сложных типов
поддерживается, если для типа реализовать TryParse . Дополнительные сведения
см. в разделе Привязка массивов и строковых значений из заголовков и строк
запроса.
Дополнительные сведения см. в статье о добавлении сводок или описаний
конечных точек.
Привязка текста запроса в виде Stream или PipeReader
Тело запроса может привязываться как Stream или PipeReader для эффективной
поддержки сценариев, в которых пользователю необходимо обрабатывать данные
и:
Хранить данные в хранилище BLOB-объектов или поставить их в очередь у
поставщика очередей.
Обрабатывать хранимые данные с помощью рабочего процесса или
облачной функции.
Например, данные могут быть помещены в очередь в Хранилище очередей Azure
или храниться в Хранилище BLOB-объектов Azure.
Дополнительные сведения см. в статье Привязка текста запроса в качестве Stream
или PipeReader.
Новые перегрузки Results.Stream
Мы ввели новые перегрузки Results.Stream для сценариев, которым необходим
доступ к базовому потоку HTTP-ответов без буферизации. Эти перегрузки также
улучшают случаи, когда API передает данные в поток ответа HTTP, например из
Хранилища BLOB-объектов Azure. В следующем примере используется
ImageSharp
для возврата уменьшенного размера указанного изображения:
C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http,
CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age=
{TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream,
token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream,
CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken:
token);
}
Дополнительные сведения см. в разделе Примеры потоковой передачи.
Типизированные результаты для минимальных API
В .NET 6 появился интерфейс IResult, который представляет значения,
возвращаемые минимальными API, не использующими неявную поддержку JSON
для сериализации возвращенного объекта для ответов HTTP. Статический класс
Results используется для создания разных объектов IResult , которые представляют
разные типы ответов. Например, установка кода статуса ответа или
перенаправление на другой URL-адрес. Однако типы, реализующие IResult ,
возвращаемые этими методами, были внутренними, что затрудняло проверку
конкретного типа IResult , возвращаемого методами в модульном тесте.
В .NET 7 типы, реализующие IResult , являются общедоступными, что позволяет
использовать утверждения типов при тестировании. Пример:
C#
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
Улучшена возможность модульного тестирования для
минимальных обработчиков маршрутов
IResult Типы реализации теперь общедоступны в
Microsoft.AspNetCore.Http.HttpResults пространстве имен. IResult Типы реализации
можно использовать для модульного тестирования минимальных обработчиков
маршрутов при использовании именованных методов вместо лямбда-выражений.
В следующем коде используется Ok<TValue> класс :
C#
[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title",
Description = "Test description",
IsDone = false
});
await context.SaveChangesAsync();
// Act
var okResult = (Ok<Todo>)await TodoEndpointsV1.GetTodo(1, context);
//Assert
Assert.Equal(200, okResult.StatusCode);
var foundTodo = Assert.IsAssignableFrom<Todo>(okResult.Value);
Assert.Equal(1, foundTodo.Id);
}
Дополнительные сведения см. в разделе IResult Типы реализации.
Новые интерфейсы HttpResult
Следующие интерфейсы в Microsoft.AspNetCore.Http пространстве имен
предоставляют способ обнаружения IResult типа во время выполнения, что
является распространенным шаблоном в реализациях фильтров:
IContentTypeHttpResult
IFileHttpResult
INestedHttpResult
IStatusCodeHttpResult
IValueHttpResult
IValueHttpResult<TValue>
Дополнительные сведения см. в разделе Интерфейсы IHttpResult.
Улучшения OpenAPI для минимальных API
пакет NuGet Microsoft.AspNetCore.OpenApi ;
Пакет Microsoft.AspNetCore.OpenApi
позволяет взаимодействовать со
спецификациями OpenAPI для конечных точек. Этот пакет создает связь между
моделями OpenAPI, определенными в пакете Microsoft.AspNetCore.OpenApi , и
конечными точками, определенными в минимальных API. Этот пакет предоставляет
API, который проверяет параметры, ответы и метаданные конечной точки и
создает тип заметки OpenAPI, подходящий для описания этой конечной точки.
C#
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
Вызов WithOpenApi с параметрами
Метод WithOpenApi
принимает функцию, которую можно использовать для
изменения заметки OpenAPI. Например, следующий код добавляет описание в
первый параметр конечной точки:
C#
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
var parameter = generatedOperation.Parameters[0];
parameter.Description = "The ID associated with the created Todo";
return generatedOperation;
});
Предоставление описаний и сводок конечных точек
Минимальные API-интерфейсы теперь поддерживают операции аннотирования с
описаниями и сводками для создания спецификаций OpenAPI. Можно вызывать
методы расширения WithDescription и WithSummary или использовать атрибуты
[EndpointDescription] и [EndpointSummary]).
Дополнительные сведения см. в статье OpenAPI в минимальных приложениях API.
Отправка файлов с помощью IFormFile и
IFormFileCollection
Минимальные API теперь поддерживают отправку файлов с применением
IFormFile и IFormFileCollection . Следующий код использует IFormFile и
IFormFileCollection, чтобы отправить файл:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Поддерживаются запросы на отправку файлов с проверкой подлинности
посредством заголовка авторизации , сертификата клиента или заголовка cookie.
Встроенная поддержка защиты от подделки отсутствует. Однако ее можно
реализовать с помощью службы IAntiforgery.
Атрибут [AsParameters] обеспечивает привязку
параметров для списков аргументов.
Атрибут[AsParameters] включает привязку параметров для списков аргументов.
Дополнительные сведения: Привязка параметров для списков аргументов с
помощью [AsParameters].
Минимальные API и контроллеры API
Новая служба сведений о проблеме
Служба сведений о проблеме IProblemDetailsService реализует интерфейс , который
поддерживает создание сведений о проблеме для API HTTP
.
Дополнительные сведения см. в разделе Служба сведений о проблеме.
Группы маршрутов
Метод MapGroup расширения помогает упорядочивать группы конечных точек с
общим префиксом. Это сокращает количество повторяющихся кодов и позволяет
настраивать целые группы конечных точек с помощью одного вызова таких
методов, как RequireAuthorization и WithMetadata , которые добавляют метаданные
конечной точки.
Например, следующий код создает две похожие группы конечных точек:
C#
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext
factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>
(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise
reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
C#
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
В этом сценарии можно использовать относительный адрес для заголовка Location
в 201 Created результате:
C#
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb
database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Первая группа конечных точек будет соответствовать только запросам с
/public/todos префиксом и будет доступна без проверки подлинности. Вторая
группа конечных точек будет соответствовать только запросам с /private/todos
префиксом и требует проверки подлинности.
Фабрика QueryPrivateTodos фильтров конечных точек — это локальная функция,
которая изменяет параметры обработчика TodoDb маршрутов, чтобы разрешить
доступ к частным данным дел и их хранение.
Группы маршрутов также поддерживают вложенные группы и сложные шаблоны
префиксов с параметрами и ограничениями маршрута. В следующем примере и
обработчик маршрутов, сопоставленный с user группой, может записывать {org}
параметры маршрута и {group} , определенные в префиксах внешней группы.
Префикс также может быть пустым. Это может быть полезно для добавления
метаданных или фильтров конечной точки в группу конечных точек без изменения
шаблона маршрута.
C#
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Добавление фильтров или метаданных в группу ведет себя так же, как и
добавление их по отдельности в каждую конечную точку перед добавлением
дополнительных фильтров или метаданных, которые могли быть добавлены во
внутреннюю группу или определенную конечную точку.
C#
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
В приведенном выше примере внешний фильтр регистрирует входящий запрос до
внутреннего фильтра, даже если он был добавлен вторым. Так как фильтры были
применены к разным группам, порядок их добавления не имеет значения. Фильтры
порядка добавляются, если они применяются к той же группе или определенной
конечной точке.
Запрос к будет регистрировать /outer/inner/ следующие данные:
Интерфейс командной строки.NET
/outer group filter
/inner group filter
MapGet filter
gRPC
Перекодирование JSON
Перекодирование gRPC JSON — это расширение для ASP.NET Core, которое создает
REST API JSON для служб gRPC. Перекодирование gRPC JSON позволяет:
Приложениям вызывать службы gRPC, используя следующие привычные
понятия HTTP.
Приложениям gRPC ASP.NET Core поддерживать gRPC и API-интерфейсы
RESTful JSON без репликации функций.
Экспериментальная поддержка создания OpenAPI из перекодированных
RESTинтерфейсов API путем интеграции со Swashbuckle.
Дополнительные сведения см. в разделах Перекодирование gRPC JSON в ASP.NET
Core приложениях gRPC и Использование OpenAPI с перекодированием gRPC
JSON ASP.NET Core приложений.
Проверки работоспособности gRPC в ASP.NET Core
Протокол проверки работоспособности gRPC
является стандартом для передачи
сведений о работоспособности серверных приложений gRPC. Приложение
предоставляет проверки работоспособности как службу gRPC. Они обычно
используются с внешней службой мониторинга для проверки состояния
приложения.
в ASP.NET Core gRPC добавлена встроенная поддержка проверок
работоспособности gRPC с пакетомGrpc.AspNetCore.HealthChecks . Результаты
проверок работоспособности .NET передаются вызывающим объектам.
Дополнительные сведения см. в разделе Проверки работоспособности gRPC в
ASP.NET Core.
Улучшена поддержка учетных данных для звонков
Учетные данные вызова — это рекомендуемый способ настройки клиента gRPC для
отправки маркера проверки подлинности на сервер. Клиенты gRPC поддерживают
две новые функции, упрощают использование учетных данных для вызова:
Поддержка учетных данных вызова с подключениями в виде открытого
текста. Ранее вызов gRPC отправлял учетные данные вызова, только если
подключение было защищено с помощью TLS. Новый параметр в
GrpcChannelOptions с именем UnsafeUseInsecureChannelCallCredentials позволяет
настраивать такое поведение. Отсутствие защиты подключения с помощью
TLS влияет на безопасность.
В фабрике клиента gRPC доступен новый метод с именем AddCallCredentials .
AddCallCredentials — это быстрый способ настройки учетных данных вызова
для клиента gRPC и хорошо интегрируется с внедрением зависимостей (DI).
Следующий код настраивает фабрику клиента gRPC для отправки Authorization
метаданных:
C#
builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.AddCallCredentials((context, metadata) =>
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
Дополнительные сведения см. в статье Настройка токена носителя с помощью
фабрики клиента gRPC.
SignalR
Получение результатов от клиента
Теперь сервер поддерживает запрос результатов от клиента. Для этого требуется,
чтобы сервер использовал ISingleClientProxy.InvokeAsync , а клиент возвращал
результат от своего обработчика .On . Строго типизированные концентраторы
также могут возвращать значения из методов интерфейса.
Дополнительные сведения см. в разделе Получение результатов от клиента.
Внедрение зависимостей для методов концентратора
SignalR
Методы концентратора SignalR теперь поддерживают внедрение служб
посредством внедрения зависимостей.
Конструкторы концентратора могут принимать службы от внедрения зависимостей
в качестве параметров, которые можно хранить в свойствах класса для
использования в методе концентратора. Дополнительные сведения см. в разделе
Внедрение служб в концентратор.
Blazor
Обработка событий изменения расположения и
состояния навигации
В .NET 7 Blazor поддерживает события изменения расположения и сохранение
состояния навигации. Это позволяет предупреждать пользователей о
несохраненных работах или выполнять связанные действия, когда пользователь
выполняет навигацию по страницам.
Дополнительные сведения см. в следующих разделах статьи Маршрутизация и
навигация :
Параметры навигации
Обработка и предотвращение изменений расположения
Пустые Blazor шаблоны проектов
Blazor имеет два новых шаблона проектов для запуска с пустого листа. Новые
шаблоны проектов App Empty и Blazor WebAssembly App Empty похожи на их
непустые аналоги, но без примера кода.Blazor Server Эти пустые шаблоны
содержат только базовую домашнюю страницу, и мы удалили начальную загрузку,
чтобы вы могли начать с другой платформы CSS.
Дополнительные сведения см. в следующих статьях:
Инструменты для ASP.NET CoreBlazor
Структура проекта ASP.NET Core Blazor
Пользовательские элементы Blazor
Пакет Microsoft.AspNetCore.Components.CustomElements
позволяет создавать
пользовательские элементы DOM на основе стандартов
с помощью Blazor.
Дополнительные сведения см. в статье Компоненты Razor ASP.NET Core.
Модификаторы привязки ( @bind:after , @bind:get ,
@bind:set )
) Важно!
В @bind:after // @bind:get @bind:set настоящее время функции получают
дополнительные обновления. Чтобы воспользоваться преимуществами
последних обновлений, убедитесь, что вы установили последнюю версию
пакета SDK.
Использование параметра обратного вызова события ( [Parameter] public
EventCallback<string> ValueChanged { get; set; } ) не поддерживается. Вместо
этого передайте Actionметод -returning или Task-returning
в/ @bind:set @bind:after .
Дополнительные сведения см. в следующих ресурсах:
Blazor@bind:after не работает в выпуске .NET 7 RTM (dotnet/aspnetcore
#44957)
BindGetSetAfter701 пример приложения (репозиторий GitHub
javiercn/BindGetSetAfter701)
В .NET 7 можно выполнять асинхронную логику после завершения события
привязки с помощью нового @bind:after модификатора. В следующем примере
PerformSearch асинхронный метод выполняется автоматически после обнаружения
любых изменений в тексте поиска:
razor
<input @bind="searchText" @bind:after="PerformSearch" />
@code {
private string searchText;
private async Task PerformSearch()
{
...
}
}
В .NET 7 также проще настроить привязку для параметров компонента.
Компоненты могут поддерживать двусторонние привязки данных, определяя пару
параметров:
@bind:get : задает значение для привязки.
@bind:set : задает обратный вызов при изменении значения.
Модификаторы @bind:get и @bind:set всегда используются вместе.
Примеры:
razor
@* Elements *@
<input type="text" @bind="text" @bind:after="() => { }" />
<input type="text" @bind:get="text" @bind:set="(value) => { }" />
<input type="text" @bind="text" @bind:after="AfterAsync" />
<input type="text" @bind:get="text" @bind:set="SetAsync" />
<input type="text" @bind="text" @bind:after="() => { }" />
<input type="text" @bind:get="text" @bind:set="(value) => { }" />
<input type="text" @bind="text" @bind:after="AfterAsync" />
<input type="text" @bind:get="text" @bind:set="SetAsync" />
@* Components *@
<InputText @bind-Value="text" @bind-Value:after="() => { }" />
<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />
<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />
<InputText @bind-Value="text" @bind-Value:after="() => { }" />
<InputText @bind-Value:get="text" @bind-Value:set="(value) => { }" />
<InputText @bind-Value="text" @bind-Value:after="AfterAsync" />
<InputText @bind-Value:get="text" @bind-Value:set="SetAsync" />
@code {
private string text = "";
private
private
private
private
void
void
Task
Task
After(){}
Set() {}
AfterAsync() { return Task.CompletedTask; }
SetAsync(string value) { return Task.CompletedTask; }
}
Дополнительные сведения о компоненте см. в InputText разделе ASP.NET Core
Blazor форм и входных компонентов.
улучшения Горячая перезагрузка
В .NET 7 Горячая перезагрузка поддержка включает следующее:
При удалении значения компоненты сбрасывают свои параметры до
значений по умолчанию.
Blazor WebAssembly:
Добавьте новые типы.
Добавление вложенных классов.
Добавьте статические методы и методы экземпляра в существующие типы.
Добавление статических полей и методов в существующие типы.
Добавление статических лямбда-выражений в существующие методы.
Добавьте лямбда-выражения, которые захватывают this существующие
методы, которые уже записаны this ранее.
Запросы динамической проверки подлинности с
помощью MSAL в Blazor WebAssembly
Новые возможности в .NET 7 Blazor WebAssembly поддерживают создание
динамических запросов проверки подлинности во время выполнения с
пользовательскими параметрами для обработки сценариев расширенной
проверки подлинности.
Дополнительные сведения см. в следующих статьях:
Защита ASP.NET Core Blazor WebAssembly
Сценарии обеспечения дополнительной безопасности ASP.NET Core Blazor
WebAssembly
Blazor WebAssembly улучшения отладки
Blazor WebAssembly Отладка имеет следующие улучшения:
Поддержка параметра Just My Code для отображения или скрытия элементов
типа, которые не входят в пользовательский код.
Поддержка проверки многомерных массивов.
Теперь в стеке вызовов отображается правильное имя для асинхронных
методов.
Улучшенная оценка выражений.
Правильная обработка ключевого слова в new производных членах.
Поддержка атрибутов, связанных с отладчиком, в System.Diagnostics .
System.Security.Cryptography поддержка в
WebAssembly
.NET 6 поддерживает семейство алгоритмов хэширования SHA при выполнении в
WebAssembly. .NET 7 обеспечивает больше алгоритмов шифрования, используя
преимущества SubtleCrypto , когда это возможно, и возвращаясь к реализации
.NET, когда SubtleCrypto ее нельзя использовать. Следующие алгоритмы
поддерживаются в WebAssembly в .NET 7:
SHA1
SHA256
SHA384
SHA512
HMACSHA1
HMACSHA256
HMACSHA384
HMACSHA512
AES-CBC
PBKDF2
HKDF
Дополнительные сведения см. в статье Разработчики, ориентированные на
browser-wasm, могут использовать API веб-шифрования (dotnet/runtime #40074).
Внедрение служб в настраиваемые атрибуты
проверки
Теперь вы можете внедрять службы в настраиваемые атрибуты проверки. Blazor
настраивает , ValidationContext чтобы его можно было использовать в качестве
поставщика услуг.
Дополнительные сведения см. в статье Формы и компоненты ввода в Blazor для
ASP.NET Core.
Input* компоненты за пределами EditContext / EditForm
Встроенные компоненты ввода теперь поддерживаются вне формы в Razor
разметке компонентов.
Дополнительные сведения см. в статье Формы и компоненты ввода в Blazor для
ASP.NET Core.
Изменения шаблона проекта
Когда в прошлом году была выпущена .NET 6, HTML-разметка _Host страницы
( Pages/_Host.chstml ) была разделена между страницей _Host и новой _Layout
страницей ( Pages/_Layout.chstml ) в шаблоне проекта .NET 6 Blazor Server .
В .NET 7 html-разметка повторно комбинирована со страницей _Host в шаблонах
проектов.
В шаблоны проектов было внесено несколько дополнительных Blazor изменений.
Невозможно перечислить все изменения в шаблонах в документации. Сведения о
переносе приложения в .NET 7 для внедрения всех изменений см. в статье
Миграция с ASP.NET Core 6.0 на 7.0.
Экспериментальный QuickGrid компонент
Новый QuickGrid компонент предоставляет удобный компонент сетки данных для
наиболее распространенных требований, а также в качестве эталонной
архитектуры и базовых показателей производительности для всех, кто создает
Blazor компоненты сетки данных.
Дополнительные сведения см. в статье Компоненты Razor ASP.NET Core.
Динамическая демонстрация: QuickGrid для Blazor примера приложения
Усовершенствования виртуализации
Усовершенствования виртуализации в .NET 7:
Компонент Virtualize поддерживает использование самого документа в
качестве корня прокрутки в качестве альтернативы применению какого-либо
другого элемента overflow-y: scroll .
Если компонент Virtualize помещается в элемент, которому требуется
определенное имя дочернего тега, SpacerElement разрешает получить или
задать имя тега разделителя в виртуализации.
Дополнительные сведения см. в следующих разделах статьи Виртуализация :
Виртуализация на корневом уровне
Управление именем тега элемента разделителя
MouseEventArgs Обновления
MovementX и MovementY добавлены в MouseEventArgs .
Дополнительные сведения см. в статье Обработка событий Blazor в ASP.NET Core.
Новая Blazor страница загрузки
Шаблон Blazor WebAssembly проекта имеет новый пользовательский интерфейс
загрузки, показывающий ход загрузки приложения.
Дополнительные сведения см. в статье Запуск ASP.NET Core Blazor.
Улучшенная диагностика для проверки подлинности в
Blazor WebAssembly
Для диагностики проблем с проверкой подлинности в Blazor WebAssembly
приложениях доступно подробное ведение журнала.
Дополнительные сведения см. Blazor в разделе ведение журнала ASP.NET Core.
Взаимодействие с JavaScript в WebAssembly
API взаимодействия JavaScript [JSImport] / [JSExport] — это новый низкоуровневый
механизм для использования .NET в Blazor WebAssembly приложениях на основе
JavaScript и . С помощью этой новой возможности взаимодействия JavaScript
можно вызывать код .NET из JavaScript с помощью среды выполнения .NET
WebAssembly и вызывать функции JavaScript из .NET без какой-либо зависимости от
Blazor модели компонентов пользовательского интерфейса.
Дополнительные сведения
Javascript JS Импорт иJS экспорт взаимодействия с ASP.NET CoreBlazor
WebAssembly: относится только к приложениямBlazor WebAssembly.
Запуск .NET из JavaScript: относится только к приложениям JavaScript, которые
не зависят от модели компонентов пользовательского Blazor интерфейса.
Условная регистрация поставщика состояния
проверки подлинности
До выпуска .NET 7 AuthenticationStateProvider был зарегистрирован в контейнере
службы с AddScoped помощью . Это усложняет отладку приложений, так как при
предоставлении пользовательской реализации требуется определенный порядок
регистрации служб. Из-за изменений внутренней платформы с течением времени
больше не нужно регистрировать AuthenticationStateProvider в AddScoped .
В коде разработчика внесите следующие изменения в регистрацию службы
поставщика состояния проверки подлинности:
diff
- builder.Services.AddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
+ builder.Services.TryAddScoped<AuthenticationStateProvider,
ExternalAuthStateProvider>();
В предыдущем примере ExternalAuthStateProvider — это реализация службы
разработчика.
Улучшения средств сборки .NET WebAssembly
Новые функции рабочей нагрузки wasm-tools для .NET 7, которые помогают
повысить производительность и обрабатывать исключения:
Поддержка webAssembly Single Instruction, Multiple Data (SIMD)
(только с
AOT, не поддерживается Apple Safari)
Поддержка обработки исключений WebAssembly
Дополнительные сведения см. в статье Инструментарий для ASP.NET Core Blazor.
Blazor Hybrid
Внешние URL-адреса
Добавлен параметр, позволяющий открывать внешние веб-страницы в браузере.
Дополнительные сведения см. в статье Маршрутизация ASP.NET Core Blazor Hybrid
и навигация.
Безопасность
Для сценариев безопасности доступно Blazor Hybrid новое руководство.
Дополнительные сведения см. в следующих статьях:
Проверка подлинности и авторизация в Blazor Hybrid ASP.NET Core
Вопросы обеспечения безопасности в ASP.NET Core Blazor Hybrid
Производительность
ПО промежуточного слоя для кэширования выходных
данных
Кэширование выходных данных — это новое ПО промежуточного слоя, которое
хранит ответы от веб-приложения и обслуживает их из кэша, а не вычисляет их
каждый раз. Кэширование выходных данных отличается от кэширования ответов
следующими способами:
Поведение кэширования настраивается на сервере.
Записи кэша можно программно сделать недействительными.
Блокировка ресурсов снижает риск штамповки кэша
и грома стада .
Повторная проверка кэша означает, что сервер может возвращать 304 Not
Modified код состояния HTTP вместо кэшированного текста ответа.
Среда хранения кэша является расширяемой.
Дополнительные сведения см. в разделе ОбзорПО промежуточного слоя для
кэширования и кэширования выходных данных.
Улучшения HTTP/3
Этот выпуск:
Обеспечивает полную поддержку HTTP/3 ASP.NET Core, она больше не
является экспериментальной.
Улучшена Kestrelподдержка HTTP/3. Двумя основными областями улучшения
являются четность функций с HTTP/1.1 и HTTP/2, а также производительность.
Обеспечивает полную поддержку UseHttps(ListenOptions, X509Certificate2) с
http/3. Kestrelпредлагает дополнительные параметры для настройки
сертификатов подключения, например подключение к указанию имени
сервера (SNI).
Добавлена поддержка HTTP/3 в HTTP.sys и IIS.
В следующем примере показано, как использовать обратный вызов SNI для
разрешения параметров TLS:
C#
using
using
using
using
Microsoft.AspNetCore.Server.Kestrel.Core;
Microsoft.AspNetCore.Server.Kestrel.Https;
System.Net.Security;
System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listenOptions.UseHttps(new TlsHandshakeCallbackOptions
{
OnConnection = context =>
{
var options = new SslServerAuthenticationOptions
{
ServerCertificate =
MyResolveCertForHost(context.ClientHelloInfo.ServerName)
};
return new ValueTask<SslServerAuthenticationOptions>
(options);
},
});
});
});
В .NET 7 была проделана значительная работа по сокращению выделений HTTP/3.
Некоторые из этих улучшений можно увидеть в следующих запросах на
вытягивание GitHub:
HTTP/3. Избегайте выделения маркеров отмены для каждого запроса
HTTP/3: предотвращение выделения connectionAbortedException
HTTP/3: пул ValueTask
Улучшения производительности HTTP/2
.NET 7 представляет значительную переработанную архитектуру того, как Kestrel
обрабатывает запросы HTTP/2. В приложениях ASP.NET Core с загруженными
подключениями HTTP/2 будет наблюдаться снижение использования ЦП и
повышение пропускной способности.
Ранее реализация мультиплексирования HTTP/2 зависела от блокировки,
контролирующей то, какой запрос может выполнять запись в базовое
подключение TCP. Потокобезопасная очередь
заменяет блокировку записи.
Теперь вместо того, чтобы потоки конкурировали за использование блокировки
записи, запросы ставятся в очередь, и их обрабатывает выделенный потребитель.
Ранее потраченные ресурсы ЦП были доступными для остальной части
приложения.
Одним из мест, где можно заметить эти улучшения, является gRPC, популярная
платформа RPC, использующая HTTP/2. Показатели Kestrel + gRPC показывают
существенное улучшение:
В коде записи кадра HTTP/2 были внесены изменения, повышающие
производительность при наличии нескольких потоков, пытающихся записать
данные по одному подключению HTTP/2. Теперь мы отправим работу TLS в пул
потоков и быстрее освобождаем блокировку записи, которую другие потоки могут
получить для записи своих данных. Сокращение времени ожидания может
значительно повысить производительность в случаях, когда существует состязание
за эту блокировку записи. Тест производительности gRPC с 70 потоками по одному
подключению (с TLS) показал улучшение запросов в секунду (RPS) на 15 %
благодаря этому изменению.
Поддержка WebSockets по HTTP/2
В .NET 7 реализована поддержка Websocket через HTTP/2 для Kestrel, SignalR
клиента JavaScript и SignalR с Blazor WebAssembly.
При использовании соединений WebSocket по HTTP/2 доступны новые
возможности, в том числе следующие:
сжатие заголовка;
мультиплексирование, которое сокращает время и ресурсы, необходимые для
выполнения нескольких запросов к серверу.
Эти функции доступны в Kestrel на всех платформах с поддержкой HTTP/2.
Согласование версии выполняется в браузерах и Kestrel автоматически, поэтому
новые интерфейсы API не требуются.
Дополнительные сведения см. в разделе Поддержка Http/2 WebSocket.
Улучшенная производительность Kestrel на
компьютерах с большим количеством ядер
Kestrel использует ConcurrentQueue<T> для многих целей. Одна из них —
планирование операций ввода-вывода при транспортировке Kestrel на основе
сокетов по умолчанию. Секционирование ConcurrentQueue на основе связанного
сокета сокращает состязание и увеличивает пропускную способность на
компьютерах с большим количеством ядер ЦП.
Профилирование на компьютерах с большим количеством ядер на .NET 6
продемонстрировало значительный показатель состязания в одном из других
экземпляров ConcurrentQueue Kestrel, в пуле PinnedMemoryPool , который Kestrel
использует для кэширования буферов байтов.
В .NET 7 пул памяти Kestrel секционируется так же, как и его очередь ввода-вывода,
что значительно сокращает состязание и увеличивает пропускную способность на
компьютерах с большим количеством ядер. На виртуальных машинах ARM64 с
80 ядрами наблюдается повышение числа ответов в секунду (RPS) более чем на
500 % по результатам теста производительности TechEmpower с открытым текстом.
На виртуальных машинах AMD с 48 ядрами повышение составляет почти 100 % по
результатам теста производительности JSON для HTTPS.
Событие ServerReady для измерения времени запуска
Приложения, использующие EventSource, могут измерять время запуска, чтобы
определить и оптимизировать его производительность. Новое событие
ServerReady
в Microsoft.AspNetCore.Hosting представляет точку, в которой сервер
готов отвечать на запросы.
Сервер
Событие New ServerReady для измерения времени
запуска
Событие ServerReady
было добавлено для измерения времени запуска
приложений ASP.NET Core.
Службы IIS
Теневое копирование в IIS
Теневое копирование сборок приложений в модуль ASP.NET Core (ANCM) для IIS
может быть удобнее для пользователя, чем остановка приложения путем
развертывания автономного файла приложения.
Дополнительные сведения см. в разделе Теневое копирование в IIS.
Прочее
Kestrel полные улучшения цепочки сертификатов
HttpsConnectionAdapterOptions имеет новое свойство ServerCertificateChain типа
X509Certificate2Collection, которое упрощает проверку цепочек сертификатов,
позволяя указывать полную цепочку, включая промежуточные сертификаты.
Дополнительные сведения см. в разделе dotnet/aspnetcore#21513
.
dotnet watch
Улучшенные выходные данные консоли для dotnet watch
Выходные данные консоли из dotnet watch были улучшены, чтобы соответствовать
ведению журнала в ASP.NET Core, и дополнены 😮эмодзи😍.
Вот пример того, как выглядят новые выходные данные:
Дополнительные сведения см. в этом запросе на вытягивание на GitHub .
Настройка dotnet watch для перезапуска при грубых
изменениях
Грубые изменения — это изменения, которые не подлежат горячей перезагрузке.
Чтобы настроить dotnet watch для перезапуска без запроса в случае грубых
изменений, задайте для переменной среды DOTNET_WATCH_RESTART_ON_RUDE_EDIT
значение true .
Темный режим страницы исключений для
разработчиков
Поддержка темного режима была добавлена на страницу исключений для
разработчиков благодаря участию Патрика Вестерхоффа
(Patrick Westerhoff).
Чтобы протестировать темный режим в браузере, на странице инструментов
разработчика установите темный режим. Например, в Firefox:
В Chrome:
Параметр шаблона проекта для использования
метода Program.Main вместо инструкций верхнего
уровня
В шаблонах .NET 7 можно не использовать инструкции верхнего уровня и не
создавать namespace и метод Main , объявенный в классе Program .
С помощью .NET CLI задайте параметр --use-program-main :
Интерфейс командной строки.NET
dotnet new web --use-program-main
В Visual Studio установите флажок Не использовать инструкции верхнего уровня
при создании проекта:
Обновленные шаблоны Angular и React
Шаблон проекта Angular обновлен до Angular 14. Шаблон проекта React обновлен
до React 18.2.
Управление токенами JSON Web Token при разработке
с использованием dotnet user-jwts
Новое средство командной строки dotnet user-jwts позволяет создавать
локальные токены JSON Web Token
(JWT) и управлять ими. Дополнительные
сведения см. в статье Управление токенами JSON Web Token при разработке с
использованием dotnet user-jwts.
Поддержка дополнительных заголовков запросов в
W3CLogger
Теперь вы можете указывать дополнительные заголовки запросов для регистрации
при использовании средства ведения журнала W3C. Для этого вызовите
AdditionalRequestHeaders() для класса W3CLoggerOptions:
C#
services.AddW3CLogging(logging =>
{
logging.AdditionalRequestHeaders.Add("x-forwarded-for");
logging.AdditionalRequestHeaders.Add("x-client-ssl-protocol");
});
Дополнительные сведения см. в разделе Параметры W3CLogger.
Распаковка запросов
Новое ПО промежуточного слоя для распаковки запросов предоставляет такие
преимущества:
позволяет конечным точкам API принимать запросы со сжатым содержимым;
использует заголовок HTTP Content-Encoding , чтобы автоматически
обнаруживать и распаковывать запросы, содержащие сжатое содержимое;
устраняет необходимость писать код для обработки сжатых запросов.
Дополнительные сведения см. в разделе ПО промежуточного слоя для распаковки
запросов.
Новые возможности в ASP.NET
Core 6.0
Статья • 28.01.2023 • Чтение занимает 27 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 6.0 со
ссылками на соответствующую документацию.
Улучшения ASP.NET Core MVC и Razor
Минимальные API
Архитектура минимальных API позволяет создавать API для HTTP с минимальным
числом зависимостей. Они идеально подходят для микрослужб и приложений,
которым нужен небольшой набор файлов, компонентов и зависимостей на
платформе ASP.NET Core. Дополнительные сведения см. в разделе:
Руководство. Создание минимального API с помощью ASP.NET Core
Различия между минимальными API и обычными API с контроллерами
Краткий справочник по минимальным API
Примеры кода перенесены на новую минимальную модель размещения в 6.0
SignalR
Тег длительно выполняемого действия для
подключений SignalR
SignalR использует новый объект
Microsoft.AspNetCore.Http.Features.IHttpActivityFeature.Activity для добавления тега
http.long_running к действию запроса. IHttpActivityFeature.Activity используется
службами APM
, например Azure Monitor Application Insights для фильтрации
запросов SignalR на создание предупреждений о длительных запросах.
Улучшения производительности SignalR
Распределить HubCallerClients
вызова метода концентратора.
один раз на соединение вместо каждого
Устранено выделение закрытия в SignalR DefaultHubDispatcher.Invoke .
Состояние передается в локальную статическую функцию через параметры,
чтобы избежать выделения закрытия. Дополнительные сведения см. в этом
запросе на вытягивание на GitHub
.
Выделение одного StreamItemMessage на поток, а не на каждый элемент
потока в потоковой передаче с сервера клиенту. Дополнительные сведения
см. в этом запросе на вытягивание на GitHub .
Компилятор Razor
Компилятор Razor обновлен для использования
генераторов исходного кода
Теперь компилятор Razor основан на генераторах исходного кода C#. Генераторы
исходного кода запускаются во время компиляции и проверяют, что
компилируется для создания дополнительных файлов, компилируемых вместе с
остальной частью проекта. Использование генераторов исходного кода упрощает
компилятор Razor и значительно сокращает время сборки.
Компилятор Razor больше не создает отдельную
сборку Views
В компиляторе Razor ранее использовался двухэтапный процесс компиляции, при
котором создавалась отдельная сборка Views, содержащая созданные
представления и страницы (файлы .cshtml ), определенные в приложении.
Созданные типы были общедоступными и находились в пространстве имен
AspNetCore .
Обновленный компилятор Razor создает представления и типы страниц в главной
сборке проекта. Эти типы теперь по умолчанию создаются как внутренние
запечатанные в пространстве имен AspNetCoreGeneratedDocument . Это изменение
улучшает производительность сборки, позволяет развертывать один файл и
позволяет этим типам участвовать в Горячей перезагрузке.
Дополнительные сведения об этом изменении см. в этой статье об ошибке
GitHub.
на
улучшения производительности ASP.NET
Core и API
Внесено множество изменений для уменьшения количества выделений и
повышения производительности в стеке:
Метод расширения app.Use без выделения. Новая перегрузка метода app.Use
требует передачи контекста в next , который сохраняет два внутренних
выделения памяти для каждого запроса, которые трубеются при другой
перегрузке.
Уменьшение количества распределений памяти при получении доступа к
HttpRequest.Cookies. Дополнительные сведения см. в этой статье об ошибке на
GitHub .
Используйте LoggerMessage.Define только для веб-сервера HTTP.sys Windows.
Вызовы методов расширения ILogger были заменены вызовами
LoggerMessage.Define .
Снизить накладные расходы на каждое соединение в SocketConnection
на
~30 %. Дополнительные сведения см. в этом запросе на вытягивание на
GitHub .
Сокращено количество выделений за счет удаления делегатов ведения
журналов в универсальных типах. Дополнительные сведения см. в этом
запросе на вытягивание на GitHub
.
Более быстрый доступ GET (примерно на 50% быстрее) к часто используемым
функциям, включая IHttpRequestFeature, IHttpResponseFeature,
IHttpResponseBodyFeature, IRouteValuesFeature и IEndpointFeature.
Дополнительные сведения см. в этом запросе на вытягивание на GitHub .
Использование строк одного экземпляра для известных имен заголовков,
даже если они не находятся в сохраненном блоке заголовка. Использование
строки с одним экземпляром помогает предотвратить несколько дубликатов
одной строки в долгосрочных соединениях, например в
Microsoft.AspNetCore.WebSockets. Дополнительные сведения см. в этой статье
об ошибке на GitHub .
Повторно используйте HttpProtocol CancellationTokenSource
в Kestrel.
Используйте новый метод CancellationTokenSource.TryReset для
CancellationTokenSource , чтобы повторно использовать токены, если они не
были отменены. Дополнительные сведения см. в описании этой ошибки
на
GitHub и в этом видео .
Реализуйте и используйте AdaptiveCapacityDictionary
Microsoft.AspNetCore.HttpЗапросCookieКоллекция
в пункте
для более эффективного
доступа к словарям. Дополнительные сведения см. в этом запросе на
вытягивание на GitHub
.
Уменьшение объема занимаемой памяти для
бездействующих подключений TLS
Для длительных подключений TLS с редко передаваемыми данными мы
значительно уменьшили объем памяти, занимаемый приложениями ASP.NET Core
в .NET 6. Это поможет улучшить масштабируемость таких сценариев, как серверы
WebSocket. Это стало возможно благодаря многочисленным улучшениям в
System.IO.Pipelines, SslStream и Kestrel. В следующих разделах подробно описаны
некоторые улучшения, которые привели к уменьшению объема занимаемой
памяти.
Уменьшение размера System.IO.Pipelines.Pipe
Для каждого установленного соединения в Kestrel выделяются два канала:
транспортный уровень к приложению для запроса;
уровень приложения к транспортировке для ответа.
За счет уменьшения размера System.IO.Pipelines.Pipe с 368 байт до 264 байт
(примерно на 28,2%) обеспечивается экономия 208 байт на подключение (104 байт
на канал).
SocketSender пула
Объекты SocketSender (подкласс SocketAsyncEventArgs) имеют размер около 350
байт во время выполнения. Вместо выделения нового объекта SocketSender для
каждого соединения их можно поместить в пул. Объекты SocketSender можно
поместить в пул, так как отправка обычно выполняется очень быстро.
Использование пулов сокращает затраты на подключение. Вместо выделения 350
байт на одно соединение, выделяется только 350 байт на IOQueue . Выделение
выполняется по очереди во избежание конкуренции. На нашем сервер WebSocket
с 5000 неактивными подключениями вместо выделения 1,75 МБ (350 байт * 5000)
выделяется 2,8 КБ (350 байт * 8) для объектов SocketSender .
Операции чтения нуля байтов с SslStream
Операции чтения без буферизации — это методика, применяемая в ASP.NET Core,
чтобы избежать аренды памяти из пула памяти, если на сокете нет доступных
данных. До этого изменения наш сервер WebSocket с 5000 неактивными
подключениями требовал 200 МБ без TLS по сравнению с 800 МБ с TLS. Некоторые
из этих выделений (4 КБ на подключение) были связаны с необходимостью Kestrel
хранить ArrayPool<T> в буфере во время ожидания завершения операций чтения
SslStream. Учитывая, что эти соединения были неактивными, ни одна из операций
чтения не завершилась и не вернула буфер в ArrayPool , из-за чего ArrayPool
нужно было выделять больше памяти. Оставшиеся выделения памяти были в
самом SslStream : 4 КБ для подтверждений TLS и буфер 32 КБ для обычных
операций чтения. В .NET 6, когда пользователь выполняет чтение нуля байтов из
SslStream и не имеет доступных данных, SslStream внутренне выполняет чтение
нуля байтов в базовом потоке. В лучшем случае (неактивное подключение) эти
изменения приводят к экономии 40 КБ на подключение, одновременно позволяя
объекту-получателю (Kestrel) получать уведомления о доступности данных без
удержания неиспользуемых буферов.
Операции чтения нуля байтов с PipeReader
Поскольку для SslStream теперь поддерживаются операции чтения без
буферизации, была добавлена возможность выполнять операции чтения нуля
байтов в StreamPipeReader , внутреннем типе, который адаптирует Stream в
PipeReader . В Kestrel StreamPipeReader используется для адаптации базового
SslStream в PipeReader . Поэтому было необходимо предоставить эту семантику
чтения нуля байтов в PipeReader .
Теперь можно создать объект PipeReader , который поддерживает чтение нуля байт
для любого используемого потока Stream , поддерживающего соответствующую
семантику (например, SslStream , NetworkStream и др.). Для создания используйте
следующий API:
Интерфейс командной строки.NET
var reader = PipeReader.Create(stream, new
StreamPipeReaderOptions(useZeroByteReads: true));
Удаление slab из SlabMemoryPool
Чтобы уменьшить фрагментацию кучи, в Kestrel применялась методика выделения
slab памяти размером 128 КБ в составе пула памяти. Затем эти slab делились на
блоки размером 4 КБ, которые использовались внутри Kestrel. Требовался размер
этих slab больше 85 КБ, чтобы принудительно выделить память в куче больших
объектов и предотвратить перемещение этого массива в ходе сборки мусора.
Однако с появлением нового поколения сборки мусора, кучи закрепленных
объектов
, больше не имеет смысла распределять блоки на slab. Kestrel теперь
напрямую выделяет блоки на куче закрепленных объектов, уменьшая сложность,
связанную с управлением пулом памяти. Это изменение должно упростить
выполнение будущих улучшений, например упростить уменьшение пула памяти,
используемого Kestrel.
Поддержка IAsyncDisposable
Теперь контроллеры, Razor Pages и компоненты представления могут использовать
IAsyncDisposable. К соответствующим интерфейсам в фабриках и активаторах были
добавлены асинхронные версии:
Новые методы предлагают реализацию интерфейса по умолчанию, которая
делегирует синхронную версию и вызывает Dispose.
Реализации переопределяют реализацию по умолчанию и обрабатывают
реализации уничтожения IAsyncDisposable .
Если реализован IAsyncDisposable и IDisposable , реализации предпочитают
первый интерфейс.
Расширители должны переопределять новые методы, включаемые для
поддержки экземпляров IAsyncDisposable .
IAsyncDisposable полезно использовать при работе с:
асинхронными перечислителями, например в асинхронных потоках;
неуправляемыми ресурсами, имеющими ресурсоемкие операции вводавывода, требующие освобождения ресурсов.
При реализации этого интерфейса используйте метод DisposeAsync для
освобождения ресурсов.
Рассмотрим контроллер, создающий и использующий Utf8JsonWriter.
Utf8JsonWriter является ресурсом IAsyncDisposable :
C#
public class HomeController : Controller, IAsyncDisposable
{
private Utf8JsonWriter? _jsonWriter;
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
_jsonWriter = new Utf8JsonWriter(new MemoryStream());
}
IAsyncDisposable должен реализовывать DisposeAsync :
C#
public async ValueTask DisposeAsync()
{
if (_jsonWriter is not null)
{
await _jsonWriter.DisposeAsync();
}
_jsonWriter = null;
}
Порт Vcpkg для клиента C++ SignalR
Vcpkg
— это кроссплатформенный диспетчер пакетов командной строки для
библиотек C и C++. Мы недавно добавили порт в vcpkg , чтобы обеспечить
нативную поддержку CMake для клиента C++ SignalR. vcpkg также работает с
MSBuild.
Клиент SignalR можно добавить в проект CMake с помощью следующего
фрагмента кода, если vcpkg включен в файл цепочки инструментов:
Интерфейс командной строки.NET
find_package(microsoft-signalr CONFIG REQUIRED)
link_libraries(microsoft-signalr::microsoft-signalr)
С помощью предыдущего фрагмента SignalRклиент C++ готов к использованию
#include
и используется в проекте без дополнительной конфигурации. Полный
пример приложения на C++, использующего SignalRклиент C++, можно найти в
репозитории halter73/SignalR-Client-Cpp-Sample
Blazor
Изменения шаблона проекта
.
Для приложений Blazor внесено несколько изменений в шаблон проекта, в
частности, файл Pages/_Layout.cshtml использовался для компоновки
содержимого, которое в более ранних приложениях Blazor Server отображалось в
файле _Host.cshtml . Изучите изменения, создав приложение из шаблона
проекта 6.0 или обратившись к справочному источнику ASP.NET Core по шаблонам
проектов:
Blazor Server
Blazor WebAssembly
Поддержка зависимостей в машинном коде Blazor
WebAssembly
Приложения Blazor WebAssembly могут использовать зависимости в машинном
коде, созданные для выполнения в WebAssembly. Дополнительные сведения. см. в
статье Собственные зависимости Blazor WebAssembly ASP.NET Core.
Компиляция AOT и повторная компоновка во время
выполнения WebAssembly
Blazor WebAssembly поддерживает компиляцию в режиме Ahead Of Time (AOT), в
котором код .NET можно скомпилировать непосредственно в WebAssembly.
Компиляция AOT позволяет повысить производительность среды выполнения за
счет увеличения размера приложения. Повторная компоновка среды выполнения
.NET WebAssembly обрезает неиспользуемый код среды выполнения и тем самым
повышает скорость загрузки. Дополнительные сведения см. в разделах Компиляция
AOT и повторная компоновка.
Сохранение предварительно отрисованного состояния
Blazor поддерживает сохранение состояния на предварительно отображенной
странице, чтобы не нужно было повторно создавать состояние при полной
загрузке приложения. Дополнительные сведения см. в статье Компоненты Razor
для предварительной визуализации и интеграции ASP.NET Core.
Границы ошибок
Границы ошибок предоставляют удобный подход к обработке исключений на
уровне пользовательского интерфейса. Дополнительные сведения см. в статье
Обработка ошибок в приложениях Blazor ASP.NET Core.
Поддержка SVG
Элемент <foreignObject>
поддерживается для отображения внутри SVG
произвольного HTML-кода. Дополнительные сведения см. в статье Компоненты
Razor ASP.NET Core.
Поддержка Blazor Server для передачи массива байтов
во взаимодействии с JS
Blazor поддерживает оптимизированное взаимодействие с JS через массивы
байтов, что позволяет избежать кодирования и декодирования массивов байтов в
Base64. Дополнительные сведения см. в следующих ресурсах:
Вызов функций JavaScript из методов .NET в ASP.NET Core Blazor
Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor
Усовершенствования строк запросов
Улучшена поддержка работы со строками запросов. Дополнительные сведения см.
в статье Маршрутизация ASP.NET Core Blazor и навигация.
Привязка для выбора нескольких элементов
Привязка поддерживает выбор нескольких параметров с элементами <input> .
Дополнительные сведения см. в следующих ресурсах:
Привязка к данным Blazor в ASP.NET Core
Формы и компоненты ввода в Blazor для ASP.NET Core
Управление содержимым элемента <head>
Компоненты Razor могут изменять содержимое элемента HTML <head> страницы, в
том числе задавать заголовок страницы (элемент <title> ) и изменять метаданные
(элементы <meta> ). Дополнительные сведения см. в разделе Управление
содержимым <head> в приложениях Blazor ASP.NET Core.
Создание компонентов Angular и React
Создавать компоненты JavaScript, характерные для платформы, можно из
компонентов Razor для веб-платформ, таких как Angular или React.
Дополнительные сведения см. в статье Компоненты Razor ASP.NET Core.
Отрисовка компонентов из JavaScript
Динамическая отрисовка компонентов Razor из JavaScript для существующих
приложений JavaScript. Дополнительные сведения см. в статье Компоненты Razor
ASP.NET Core.
Пользовательские элементы
Доступна экспериментальная поддержка для создания пользовательских
элементов, использующих стандартные интерфейсы HTML. Дополнительные
сведения см. в статье Компоненты Razor ASP.NET Core.
Выведение универсальных типов компонентов из
компонентов-предков
Компонент-предок может каскадировать параметр типа по имени и отображать
потомки с помощью нового атрибута [CascadingTypeParameter] . Дополнительные
сведения см. в статье Компоненты Razor ASP.NET Core.
Динамически отображаемые компоненты
Используйте новый встроенный компонент DynamicComponent для отображения
компонентов по типу. Дополнительные сведения см. в разделе Динамически
отображаемые компоненты Razor ASP.NET Core.
Улучшенная доступность Blazor
Используйте новый компонент FocusOnNavigate , чтобы установить фокус
пользовательского интерфейса на элемент на основе селектора CSS после
перехода с одной страницы на другую. Дополнительные сведения см. в статье
Маршрутизация ASP.NET Core Blazor и навигация.
Поддержка аргументов пользовательских событий
Blazor поддерживает аргументы пользовательских событий, которые позволяют
передавать произвольные данные в обработчики событий .NET с
пользовательскими событиями. Дополнительные сведения см. в статье Обработка
событий Blazor в ASP.NET Core.
Необходимые параметры
Примените новый атрибут [EditorRequired] , чтобы указать обязательный параметр
компонента. Дополнительные сведения см. в статье Компоненты Razor ASP.NET
Core.
Совместное размещение файлов JavaScript со
страницами, представлениями и компонентами
Размещайте совместно файлы JavaScript для страниц, представлений и
компонентов Razor — это удобный способ организации скриптов в приложении.
Дополнительные сведения см. в разделе Взаимодействие JavaScript приложения
ASP.NET Core (взаимодействие JS).
Инициализаторы JavaScript
Инициализаторы JavaScript выполняют логику до и после загрузки приложения
Blazor. Дополнительные сведения см. в разделе Взаимодействие JavaScript
приложения ASP.NET Core (взаимодействие JS).
Потоковая передача при взаимодействии с JavaScript
Blazor теперь поддерживает потоковую передачу данных между .NET и JavaScript.
Дополнительные сведения см. в следующих ресурсах:
Потоковая передача из .NET в JavaScript
Потоковая передача из JavaScript в .NET
Ограничения универсального типа
Теперь поддерживаются параметры универсального типа. Дополнительные
сведения см. в статье Компоненты Razor ASP.NET Core.
Макет развертывания WebAssembly
Используйте макет развертывания, чтобы включить загрузку приложений Blazor
WebAssembly в ограниченных средах безопасности. Дополнительные сведения см.
в разделе Макет развертывания для приложений Blazor WebAssembly ASP.NET Core.
Новые статьи о Blazor
В дополнение к функциям Blazor, описанным в предыдущих разделах, доступны
новые статьи о Blazor по следующим темам:
Загрузки файлов Blazor ASP.NET Core: узнайте, как скачать файл, используя
собственное взаимодействие byte[] для потоковой передачи, чтобы
обеспечить эффективную передачу данных клиенту.
Работа с изображениями в Blazor ASP.NET Core: узнайте, как работать с
изображениями в приложениях Blazor, включая способ потоковой передачи
данных изображения и предварительный просмотр изображения.
Создание приложений Blazor Hybrid с
помощью .NET MAUI, WPF и Windows Forms
Используйте Blazor Hybrid для объединения настольных и мобильных собственных
клиентских платформ с .NET и Blazor:
.NET Multi-platform App UI (.NET MAUI) представляет собой кроссплатформенную платформу для создания собственных мобильных и
классических приложений с помощью C# и XAML.
Приложения Blazor Hybrid можно создавать с помощью платформ Windows
Presentation Foundation (WPF) и Windows Forms.
) Важно!
Платформа Blazor Hybrid находится на этапе предварительной версии и не
должна использоваться в рабочих приложениях до финального выпуска.
Дополнительные сведения см. в следующих ресурсах:
Ознакомьтесь с документацией по ASP.NET Core Blazor Hybrid
Что такое .NET MAUI?
Блог Microsoft .NET (категория: ".NET MAUI")
Kestrel
HTTP/3
в настоящее время находится в разработке, поэтому подлежит
изменению. Поддержка HTTP/3 в ASP.NET Core не выпущена — это
предварительная версия функции, включенная в .NET 6.
Kestrel теперь поддерживает HTTP/3. Дополнительные сведения см. в разделе
Использование протокола HTTP/3 с веб-сервером Kestrel ASP.NET Core и запись в
блоге Поддержка протокола HTTP/3 в .NET 6
.
Новые категории ведения журнала Kestrel для
выборочного ведения журнала
До этого изменения включение подробного ведения журнала для Kestrel было
чрезмерно дорогостоящим, так как у всего Kestrel был общий доступ к имени
категории ведения журнала Microsoft.AspNetCore.Server.Kestrel .
Microsoft.AspNetCore.Server.Kestrel по-прежнему можно использовать, но
следующие новые подкатегории обеспечивают более полный контроль ведения
журнала:
Microsoft.AspNetCore.Server.Kestrel (текущая категория): ApplicationError ,
ConnectionHeadResponseBodyWrite , ApplicationNeverCompleted , RequestBodyStart ,
RequestBodyDone , RequestBodyNotEntirelyRead , RequestBodyDrainTimedOut ,
ResponseMinimumDataRateNotSatisfied , InvalidResponseHeaderRemoved ,
HeartbeatSlow .
Microsoft.AspNetCore.Server.Kestrel.BadRequests : ConnectionBadRequest ,
RequestProcessingError , RequestBodyMinimumDataRateNotSatisfied .
Microsoft.AspNetCore.Server.Kestrel.Connections : ConnectionAccepted ,
ConnectionStart , ConnectionStop , ConnectionPause , ConnectionResume ,
ConnectionKeepAlive , ConnectionRejected , ConnectionDisconnect ,
NotAllConnectionsClosedGracefully , NotAllConnectionsAborted ,
ApplicationAbortedConnection .
Microsoft.AspNetCore.Server.Kestrel.Http2 : Http2ConnectionError ,
Http2ConnectionClosing , Http2ConnectionClosed , Http2StreamError ,
Http2StreamResetAbort , HPackDecodingError , HPackEncodingError ,
Http2FrameReceived , Http2FrameSending , Http2MaxConcurrentStreamsReached .
Microsoft.AspNetCore.Server.Kestrel.Http3 : Http3ConnectionError ,
Http3ConnectionClosing , Http3ConnectionClosed , Http3StreamAbort ,
Http3FrameReceived , Http3FrameSending .
Существующие правила продолжают работать, но теперь можно более
избирательно включать правила. Например, можно значительно сократить
издержки на наблюдения, если включить ведение журнала Debug только для
недопустимых запросов. Для этого используется следующая конфигурация:
XML
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Kestrel.BadRequests": "Debug"
}
}
Фильтрация журналов применяет правила с наиболее длинным соответствующим
префиксом категории. Дополнительные сведения см. в разделе Применение
правил фильтрации.
Создание события KestrelServerOptions через
EventSource
Источник KestrelEventSource
создает новое событие, содержащее
сериализованный JSON-файл KestrelServerOptions, если он включен с уровнем
детализации EventLevel.LogAlways . Это событие упрощает работу с поведением
сервера при анализе собранных трассировок. Ниже приведен код JSON с
примером полезных данных события:
JSON
{
"AllowSynchronousIO": false,
"AddServerHeader": true,
"AllowAlternateSchemes": false,
"AllowResponseHeaderCompression": true,
"EnableAltSvc": false,
"IsDevCertLoaded": true,
"RequestHeaderEncodingSelector": "default",
"ResponseHeaderEncodingSelector": "default",
"Limits": {
"KeepAliveTimeout": "00:02:10",
"MaxConcurrentConnections": null,
"MaxConcurrentUpgradedConnections": null,
"MaxRequestBodySize": 30000000,
"MaxRequestBufferSize": 1048576,
"MaxRequestHeaderCount": 100,
"MaxRequestHeadersTotalSize": 32768,
"MaxRequestLineSize": 8192,
"MaxResponseBufferSize": 65536,
"MinRequestBodyDataRate": "Bytes per second: 240, Grace Period:
00:00:05",
"MinResponseDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
"RequestHeadersTimeout": "00:00:30",
"Http2": {
"MaxStreamsPerConnection": 100,
"HeaderTableSize": 4096,
"MaxFrameSize": 16384,
"MaxRequestHeaderFieldSize": 16384,
"InitialConnectionWindowSize": 131072,
"InitialStreamWindowSize": 98304,
"KeepAlivePingDelay": "10675199.02:48:05.4775807",
"KeepAlivePingTimeout": "00:00:20"
},
"Http3": {
"HeaderTableSize": 0,
"MaxRequestHeaderFieldSize": 16384
}
},
"ListenOptions": [
{
"Address": "https://127.0.0.1:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "https://[::1]:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://127.0.0.1:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://[::1]:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
}
]
}
Новое событие DiagnosticSource для отклоненных
HTTP-запросов
Kestrel теперь создает новое событие DiagnosticSource для HTTP-запросов,
отклоненных на уровне сервера. До этого изменения возможности наблюдать за
этими отклоненными запросами не было. Новое событие
DiagnosticSource Microsoft.AspNetCore.Server.Kestrel.BadRequest содержит объект
IBadRequestExceptionFeature, который можно использовать для анализа причины
отклонения запроса.
C#
using Microsoft.AspNetCore.Http.Features;
using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var diagnosticSource = app.Services.GetRequiredService<DiagnosticListener>
();
using var badRequestListener = new BadRequestEventListener(diagnosticSource,
(badRequestExceptionFeature) =>
{
app.Logger.LogError(badRequestExceptionFeature.Error, "Bad request
received");
});
app.MapGet("/", () => "Hello world");
app.Run();
class BadRequestEventListener : IObserver<KeyValuePair<string, object>>,
IDisposable
{
private readonly IDisposable _subscription;
private readonly Action<IBadRequestExceptionFeature> _callback;
public BadRequestEventListener(DiagnosticListener diagnosticListener,
Action<IBadRequestExceptionFeature>
callback)
{
_subscription = diagnosticListener.Subscribe(this!, IsEnabled);
_callback = callback;
}
private static readonly Predicate<string> IsEnabled = (provider) =>
provider switch
{
"Microsoft.AspNetCore.Server.Kestrel.BadRequest" => true,
_ => false
};
public void OnNext(KeyValuePair<string, object> pair)
{
if (pair.Value is IFeatureCollection featureCollection)
{
var badRequestFeature =
featureCollection.Get<IBadRequestExceptionFeature>();
if (badRequestFeature is not null)
{
_callback(badRequestFeature);
}
}
}
public void OnError(Exception error) { }
public void OnCompleted() { }
public virtual void Dispose() => _subscription.Dispose();
}
Дополнительные сведения см. в разделе Ведение журнала и диагностика в Kestrel.
Создание ConnectionContext на основе принятого
приема
SocketConnectionContextFactory позволяет создать ConnectionContext на основе
принятого сокета. Это позволяет создавать пользовательские
IConnectionListenerFactory на основе сокетов, не теряя при этом
производительность и организацию пулов в SocketConnection .
См. пример пользовательского IConnectionListenerFactory
, в котором показано,
как использовать SocketConnectionContextFactory .
Kestrel является профилем запуска по умолчанию для
Visual Studio
Профиль запуска по умолчанию для всех новых веб-проектов dotnet — Kestrel.
Запуск Kestrel значительно ускоряется, что повышает удобство при разработке
приложений.
IIS Express по-прежнему можно использовать в качестве профиля запуска для таких
сценариев, как проверка подлинности Windows или общий доступ к портам.
Порты localhost для Kestrel являются случайными
Дополнительные сведения см. в разделе Шаблон генерируемых портов для Kestrel
в этом документе.
Проверка подлинности и авторизация
Серверы проверки подлинности
В версиях .NET от 3 до 5 использовался IdentityServer4
как часть шаблона для
поддержки выдачи маркеров JWT для одностраничных приложений (SPA) и
приложений Blazor. Шаблоны теперь используют сервер Duende Identity .
Если вы расширяете модели удостоверений и обновляете существующие проекты,
необходимо обновить пространства имен в коде с IdentityServer4.IdentityServer
на Duende.IdentityServer и следовать инструкциям по миграции .
Модель лицензирования для Duende Identity Server была изменена на
перекрестную лицензию, из-за чего может потребоваться оплатить лицензию, если
она используется в рабочей среде в коммерческих целях. Дополнительные
сведения см. на странице лицензии Duende .
Отложенное согласование сертификата клиента
Теперь разработчики могут согласиться на использование отложенного
согласования сертификата клиента, указав ClientCertificateMode.DelayCertificate в
HttpsConnectionAdapterOptions. Это работает только с подключениями HTTP/1.1,
так как HTTP/2 запрещает отложенное повторное согласование сертификата.
Вызывающий объект этого API должен поместить в буфер текст запроса, прежде
чем запрашивать сертификат клиента:
C#
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.WebUtilities;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(options =>
{
options.ConfigureHttpsDefaults(adapterOptions =>
{
adapterOptions.ClientCertificateMode =
ClientCertificateMode.DelayCertificate;
});
});
var app = builder.Build();
app.Use(async (context, next) =>
{
bool desiredState = GetDesiredState();
// Check if your desired criteria is met
if (desiredState)
{
// Buffer the request body
context.Request.EnableBuffering();
var body = context.Request.Body;
await body.DrainAsync(context.RequestAborted);
body.Position = 0;
// Request client certificate
var cert = await context.Connection.GetClientCertificateAsync();
// Disable buffering on future requests if the client doesn't
provide a cert
}
await next(context);
});
app.MapGet("/", () => "Hello World!");
app.Run();
OnCheckSlidingExpiration событие для контроля
обновления cookie
Скользящий срок действия проверки подлинности Cookie теперь можно настроить
или подавить с помощью нового OnCheckSlidingExpiration. Например, это событие
может использоваться одностраничным приложением, которое должно
периодически выполнять проверку связи с сервером, не влияя на сеанс проверки
подлинности.
Прочее
Горячая перезагрузка
Горячая перезагрузка позволяет быстро вносить обновления в пользовательский
интерфейс и код работающих приложений без потери состояния приложения,
чтобы ускорить разработку и повысить ее продуктивность. Дополнительные
сведения см. в статьях Поддержка Горячей перезагрузки .NET для ASP.NET Core и
Обновление основных возможностей хода выполнения Горячей перезагрузки .NET
и Visual Studio 2022 .
Улучшенные шаблоны одностраничных приложений
(SPA)
Шаблоны проектов ASP.NET Core были обновлены для Angular и React. Теперь в
них используется улучшенный шаблон для одностраничных приложений, которые
обеспечивает большую гибкость и более точно соответствуют шаблонам для
интерфейса современных веб-приложений.
Ранее в шаблоне ASP.NET Core для Angular и React во время разработки
использовалось специализированное ПО промежуточного слоя для запуска
сервера разработки для интерфейсной платформы, а затем запросы прокси от
ASP.NET Core к серверу разработки. Логика запуска сервера разработки была
предназначена для интерфейса командной строки для соответствующей
интерфейсной платформы. Для поддержки дополнительных интерфейсных
платформ, использующих этот шаблон, нужно было добавлять дополнительную
логику в ASP.NET Core.
Обновленные шаблоны ASP.NET Core для Angular и React в .NET 6 меняют
используемую ранее схему и используют преимущества встроенной поддержки
прокси-сервера на серверах разработки большинства современных интерфейсных
платформ. При запуске приложения ASP.NET Core интерфейсный сервер
разработки запускается так же, как и раньше, но сервер разработки настроен на
передачу запросов через прокси-сервер к внутреннему процессу ASP.NET Core. Все
настройки, связанные с настройкой прокси-сервера, являются частью приложения,
а не ASP.NET Core. Настроить проект ASP.NET Core для работы с другими
интерфейсными платформами теперь просто: нужно настроить интерфейсный
сервер разработки для выбранной платформы, чтобы передавать запросы на
внутреннему процессу ASP.NET Core в соответствии с шаблонами Angular и React.
Для кода запуска приложения ASP.NET Core больше не нужна отдельная логика для
одностраничного приложения. Логика для запуска интерфейсного сервера
разработки во время разработки внедряется в приложение во время выполнения с
помощью нового пакета Microsoft.AspNetCore.SpaProxy . Резервная
маршрутизация обрабатывается с помощью маршрутизации конечных точек
вместо ПО промежуточного слоя для одностраничных приложений.
Соответствующие шаблоны все еще можно запускать в виде отдельного проекта в
Visual Studio или с помощью dotnet run из командной строки. При публикации
приложения интерфейсный код в папке ClientApp строится и собирается, как и
раньше, в корне узла приложения ASP.NET Core и предоставляется в виде
статических файлов. Скрипты, содержащиеся в шаблоне, настраивают
интерфейсный сервер разработки для использования протокола HTTPS с помощью
сертификата разработки ASP.NET Core.
Черновая поддержка HTTP/3 в .NET 6
HTTP/3
в настоящее время находится в разработке, поэтому подлежит
изменению. Поддержка HTTP/3 в ASP.NET Core не выпущена — это
предварительная версия функции, включенная в .NET 6.
См. запись блога Поддержка HTTP/3 в .NET 6
.
Аннотации ссылочного типа, допускающие
значения NULL
В частях исходного кода ASP.NET Core 6,0
были применены заметки о
допустимости значений NULL.
Используя новую функцию допустимости значений NULL в C# 8, ASP.NET Core
может обеспечить дополнительную безопасность во время компиляции при
обработке ссылочных типов. Например, защита от исключений ссылок на null . В
проектах, которые выбрали использование заметок о допустимости значений
NULL, могут появиться новые предупреждения времени сборки от API ASP.NET
Core.
Чтобы включить ссылочные типы, допускающие значения NULL, добавьте в файлы
проекта следующее свойство:
XML
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Дополнительные сведения см. в статье Ссылочные типы, допускающие значение
NULL.
Анализ исходного кода
Были добавлены несколько анализаторов платформы компилятора .NET, которые
проверяют код приложения на наличие таких проблем, как неправильная
конфигурация или порядок ПО промежуточного слоя, конфликты маршрутизации
и т. д. Дополнительные сведения см. в статье Анализ кода в приложениях ASP.NET
Core.
Улучшения шаблонов веб-приложений
Шаблоны веб-приложений:
Используют новую минимальную модель размещения.
Значительно сокращают количество файлов и строк кода, необходимых для
создания приложения. Например, пустое веб-приложение ASP.NET Core
создает один файл C# с четырьмя строками кода и является полноценным
приложением.
Объединяют Startup.cs и Program.cs в один файл Program.cs .
Используют инструкции верхнего уровня для минимизации кода,
необходимого для приложения.
Используют глобальные директивы using, чтобы исключить или
минимизировать числу требуемых строк инструкций using.
Созданные шаблоном порты для Kestrel
Во время создания проекта назначаются случайные порты для использования вебсервером Kestrel. Случайные порты помогают уменьшить конфликт портов, если на
одном компьютере выполняется несколько проектов.
При создании проекта в созданном файле Properties/launchSettings.json задается
случайный порт HTTP между 5000 и 5300 и случайный порт HTTPS между 7000 и
7300. Порты можно изменить в файле Properties/launchSettings.json . Если порт не
указан, Kestrel по умолчанию использует порт HTTP 5000 и HTTPS 5001.
Дополнительные сведения см. в статье KestrelНастройка конечных точек для вебсервера для ASP.NET Core.
Новые параметры ведения журнала по умолчанию
В appsettings.json и appsettings.Development.json были внесены следующие
изменения:
diff
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"
Изменение с "Microsoft": "Warning" на "Microsoft.AspNetCore": "Warning" приводит
к записи всех информационных сообщений из пространства имен Microsoft ,
кроме Microsoft.AspNetCore . Например, Microsoft.EntityFrameworkCore теперь
заносится в журнал на информационном уровне.
Автоматическое добавление ПО промежуточного
слоя страницы исключений разработчика
В среде разработки по умолчанию DeveloperExceptionPageMiddleware добавляется .
Больше не нужно добавлять следующий код в приложения пользовательского вебинтерфейса:
C#
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
Поддержка заголовков запросов в кодировке Latin1 в
HttpSysServer
HttpSysServer теперь поддерживает декодирование заголовков запросов, Latin1
закодированных путем присвоения свойству UseLatin1RequestHeaders в
HttpSysOptions значения true :
C#
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys(o => o.UseLatin1RequestHeaders = true);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Журналы модуля ASP.NET Core включают метки
времени и идентификатор процесса
Расширенные журналы диагностики модуля ASP.NET Core (ANCM) для IIS (ANCM)
содержат метки времени и идентификатор процесса , создающего журналы.
Запись меток времени и идентификатора процесса в журналы упрощает
диагностику проблем с перекрывающимися перезапусками процессов в IIS при
выполнении нескольких рабочих процессов IIS.
Журналы теперь похожи на пример, показанный ниже:
Интерфейс командной строки.NET
[2021-07-28T19:23:44.076Z, PID: 11020] [aspnetcorev2.dll] Initializing logs
for 'C:\<path>\aspnetcorev2.dll'. Process Id: 11020. File Version:
16.0.21209.0. Description: IIS ASP.NET Core Module V2. Commit:
96475a2acdf50d7599ba8e96583fa73efbe27912.
[2021-07-28T19:23:44.079Z, PID: 11020] [aspnetcorev2.dll] Resolving hostfxr
parameters for application: '.\InProcessWebSite.exe' arguments: '' path:
'C:\Temp\e86ac4e9ced24bb6bacf1a9415e70753\'
[2021-07-28T19:23:44.080Z, PID: 11020] [aspnetcorev2.dll] Known dotnet.exe
location: ''
Настраиваемый размер буфера для
неиспользованных входящих запросов для IIS
Сервер IIS ранее помещал в буфер только 64 КБ текстов неиспользованных
запросов. Размер буфера 64 КБ привел к ограничению операций чтения этим
максимальным размером, что влияло на производительность при больших
входящих объектах, таких как отправка данных. В .NET 6 размер буфера по
умолчанию измене с 64 КиБ на 1 МиБ, что повышает пропускную способность для
отправок больших объемов данных. В наших тестах отправка 700 МиБ, которая
занимала 9 секунд, теперь занимает всего 2,5 секунды.
Недостатком большего размера буфера является увеличение потребления памяти
для каждого запроса, когда приложение не считывает текст запроса быстро.
Поэтому, помимо изменения размера буфера по умолчанию, теперь появилась
возможность настроить размер буфера для приложений в зависимости от рабочей
нагрузки.
Вспомогательные функции тегов для компонентов
представлений
Рассмотрим компонент представления с необязательным параметром, как
показано в следующем коде:
C#
class MyViewComponent
{
IViewComponentResult Invoke(bool showSomething = false) { ... }
}
В ASP.NET Core 6 вспомогательную функцию тегов можно вызвать без указания
значения для параметра showSomething :
razor
<vc:my />
Шаблон Angular обновлен до версии Angular 12
Шаблон ASP.NET Core 6.0 для Angular теперь использует Angular 12
.
Шаблон React обновлен до React 17
.
Настраиваемое пороговое значение буфера перед
записью на диск в форматировщике выходных
данных Json.NET
Примечание. Рекомендуется использовать форматировщик выходных данных
System.Text.Json, за исключением случаев, когда сериализатор Newtonsoft.Json
требуется для обеспечения совместимости. Сериализатор System.Text.Json
является полностью асинхронным ( async ) и эффективно работает для больших
объемов полезных данных.
Форматировщик выходных данных Newtonsoft.Json по умолчанию помещает в
буфер в памяти ответы размером до 32 КБ перед буферизацией на диске. Это
позволяет избежать выполнения синхронных операций ввода-вывода, которые
могут привести к другим побочным эффектам, таким как нехватка потоков и
взаимоблокировка приложений. Однако если размер ответа превышает 32 КБ, для
операций дискового ввода-вывода требуется значительное время. Теперь
пороговое использование памяти, прежде чем данные помещаются в буфер на
диске, можно настроить с помощью свойства
MvcNewtonsoftJsonOptions.OutputFormatterMemoryBufferThreshold:
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages()
.AddNewtonsoftJson(options =>
{
options.OutputFormatterMemoryBufferThreshold = 48 * 1024;
});
var app = builder.Build();
Дополнительные сведения см. в этом запросе на вытягивание на GitHub
ив
файле NewtonsoftJsonOutputFormatterTest.cs .
Ускорение получения и установки заголовков HTTP
Добавлены новые API для предоставления доступа ко всем распространенным
заголовкам, доступным в Microsoft.Net.Http.Headers.HeaderNames в качестве
свойств (IHeaderDictionary), в результате чего API теперь проще использовать.
Например, встроенное ПО промежуточного слоя в следующем коде получает и
устанавливает заголовки запросов и ответов с помощью новых API:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Use(async (context, next) =>
{
var hostHeader = context.Request.Headers.Host;
app.Logger.LogInformation("Host header: {host}", hostHeader);
context.Response.Headers.XPoweredBy = "ASP.NET Core 6.0";
await next.Invoke(context);
var dateHeader = context.Response.Headers.Date;
app.Logger.LogInformation("Response date: {date}", dateHeader);
});
app.Run();
Для реализованных заголовков методы доступа получения и установки
реализуются путем перехода непосредственно к полю без поиска. Для
нереализованных заголовков методы доступа могут пропускать первоначальный
поиск по реализованным заголовкам и напрямую выполнять поиск
Dictionary<string, StringValues> . Пропуск поиска ускоряет доступ в обоих
сценариях.
Асинхронная потоковая передача
ASP.NET Core теперь поддерживает асинхронную потоковую передачу из действий
контроллера и ответы от форматировщика JSON. Возврат IAsyncEnumerable от
действия больше не приводит к буферизации содержимого ответа в памяти перед
его отправкой. Отсутствие буферизации помогает сократить использование памяти
при возврате больших наборов данных, которые можно перечислить асинхронно.
Обратите внимание, что Entity Framework Core предоставляет реализации
IAsyncEnumerable для запроса базы данных. Улучшенная поддержка
IAsyncEnumerable в ASP.NET Core в .NET 6 может сделать использование EF Core с
ASP.NET Core более эффективным. Например, следующий код больше не помещает
данные о продуктах в память перед отправкой ответа:
C#
public IActionResult GetMovies()
{
return Ok(_context.Movie);
}
Однако при использовании отложенной загрузки в EF Coreэто новое поведение
может привести к ошибкам из-за параллельного выполнения запроса во время
перечисления данных. Приложения могут вернуться к предыдущему поведению
путем буферизации данных:
C#
public async Task<IActionResult> GetMovies2()
{
return Ok(await _context.Movie.ToListAsync());
}
Дополнительные сведения об этом изменении в поведении см. в соответствующем
объявлении .
ПО промежуточного слоя для ведения журнала HTTP
Ведение журнала HTTP — это новое встроенное ПО промежуточного слоя, в
котором регистрируются сведения об HTTP-запросах и HTTP-ответах, включая
заголовки и весь текст:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/", () => "Hello World!");
app.Run();
При переходе к / с помощью кода выше данные записываются в журнал
примерно так, как показано ниже:
Интерфейс командной строки.NET
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
Request:
Protocol: HTTP/2
Method: GET
Scheme: https
PathBase:
Path: /
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,
*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cache-Control: max-age=0
Connection: close
Cookie: [Redacted]
Host: localhost:44372
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Edg/95.0.1020.30
sec-ch-ua: [Redacted]
sec-ch-ua-mobile: [Redacted]
sec-ch-ua-platform: [Redacted]
upgrade-insecure-requests: [Redacted]
sec-fetch-site: [Redacted]
sec-fetch-mode: [Redacted]
sec-fetch-user: [Redacted]
sec-fetch-dest: [Redacted]
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Content-Type: text/plain; charset=utf-8
Предыдущие выходные данные были включены в следующий файл
appsettings.Development.json :
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware":
"Information"
}
}
}
Ведение журнала HTTP предоставляет журналы со следующими сведениями:
информация HTTP-запроса;
общие свойства;
заголовки;
текст;
информация HTTP-ответа.
Чтобы настроить ПО промежуточного слоя для ведения журнала HTTP, укажите
HttpLoggingOptions:
C#
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
// Customize HTTP logging.
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestHeaders.Add("My-Request-Header");
logging.ResponseHeaders.Add("My-Response-Header");
logging.MediaTypeOptions.AddText("application/javascript");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});
var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/", () => "Hello World!");
app.Run();
Функция IConnectionSocketFeature
Функция запроса IConnectionSocketFeature предоставляет доступ к базовому
принимающему сокету, связанному с текущим запросом. Использовать ее можно с
помощью FeatureCollection в HttpContext .
Например, следующее приложение задает свойство LingerState для принимающего
сокета:
C#
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ConfigureEndpointDefaults(listenOptions =>
listenOptions.Use((connection, next) =>
{
var socketFeature =
connection.Features.Get<IConnectionSocketFeature>();
socketFeature.Socket.LingerState = new LingerOption(true, seconds:
10);
return next();
}));
});
var app = builder.Build();
app.MapGet("/", (Func<string>)(() => "Hello world"));
await app.RunAsync();
Ограничения универсального типа в Razor
При определении параметров универсального типа в Razor с помощью директивы
@typeparam ограничения универсального типа теперь можно указать с помощью
стандартного синтаксиса C#:
Уменьшение размера скриптов SignalR, Blazor Server и
MessagePack
Скрипты SignalR, MessagePack и Blazor Server теперь значительно меньше, что
обеспечивает меньший объем загрузки, меньшее время синтаксического анализа и
компиляции кода JavaScript в браузере и ускоряет запуск. Уменьшение размера:
signalr.js : 70%
blazor.server.js : 45%
Уменьшение размера скриптов стало возможным благодаря Бену Адамсу (Ben
Adams) . Дополнительные сведения об уменьшении размера см. в этом запросе
на вытягивание на GitHub .
Включение сеансов профилирования Redis
Вклад от Габриэля Лукачи (Gabriel Lucaci)
позволяет проводить сеансы
профилирования Redis с помощью
Microsoft.Extensions.Caching.StackExchangeRedis
:
C#
using StackExchange.Redis.Profiling;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ProfilingSession = () => new ProfilingSession();
});
Дополнительные сведения см. в статье Профилирование StackExchange.Redis .
Теневое копирование в IIS
В модуль ASP.NET Core (ANCM) для IIS добавлена экспериментальная функция,
позволяющая обеспечить поддержку теневого копирования сборок приложений. В
настоящее время .NET блокирует двоичные файлы приложения при запуске на
Windows, что делает невозможным замену двоичных файлов во время работы
приложения. Хотя мы по-прежнему рекомендуем использовать автономный файл
приложения, мы понимаем, что существуют определенные сценарии (например,
FTP-развертывания), где это невозможно.
В таких случаях включите теневое копирование, настроив параметры обработчика
модуля ASP.NET Core. В большинстве случаев у приложений ASP.NET Core нет
файла web.config в системе управления версиями, который можно изменить. В
ASP.NET Core web.config обычно создается пакетом SDK. Чтобы приступить к
работе, можно использовать следующий пример web.config :
XML
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- To customize the asp.net core module uncomment and edit the following
section.
For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->
<system.webServer>
<handlers>
<remove name="aspNetCore"/>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2"
resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%"
stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
<handlerSettings>
<handlerSetting name="experimentalEnableShadowCopy" value="true" />
<handlerSetting name="shadowCopyDirectory"
value="../ShadowCopyDirectory/" />
<!-- Only enable handler logging if you encounter issues-->
<!--<handlerSetting name="debugFile" value=".\logs\aspnetcoredebug.log" />-->
<!--<handlerSetting name="debugLevel" value="FILE,TRACE" />-->
</handlerSettings>
</aspNetCore>
</system.webServer>
</configuration>
Теневое копирование в IIS — это экспериментальная функция. Мы не гарантируем,
что она станет частью ASP.NET Core. Оставьте отзыв о теневом копировании IIS в
описании этой проблемы на GitHub .
Дополнительные ресурсы
Примеры кода перенесены на новую минимальную модель размещения в 6.0
Новые возможности .NET 6
Новые возможности в ASP.NET
Core 5.0
Статья • 05.10.2022 • Чтение занимает 13 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 5.0 со
ссылками на соответствующую документацию.
Улучшения ASP.NET Core MVC и Razor
Привязка модели DateTime в формате UTC
Привязка модели теперь поддерживает привязку строк времени в формате UTC к
DateTime . Если запрос содержит строку времени в формате UTC, привязка модели
привязывает ее к DateTime в формате UTC. Например, следующая строка времени
привязывается к DateTime в формате UTC:
https://example.com/mycontroller/myaction?time=2019-06-14T02%3A30%3A04.0576719Z
Привязка модели и проверка с помощью типов
записей C# 9
Типы записей C# 9 можно использовать с привязкой модели в контроллере MVC
или на странице Razor. Типы записей — это хороший способ моделирования
передаваемых по сети данных.
Например, в следующем контроллере PersonController используется тип записи
Person с привязкой модели и проверкой формы:
C#
public record Person([Required] string Name, [Range(0, 150)] int Age);
public class PersonController
{
public IActionResult Index() => View();
[HttpPost]
public IActionResult Index(Person person)
{
// ...
}
}
Файл Person/Index.cshtml :
CSHTML
@model Person
Name: <input asp-for="Model.Name" />
<span asp-validation-for="Model.Name" />
Age: <input asp-for="Model.Age" />
<span asp-validation-for="Model.Age" />
Усовершенствования DynamicRouteValueTransformer
В ASP.NET Core 3.1 появился DynamicRouteValueTransformer в качестве способа
использования пользовательской конечной точки для динамического выбора
действия контроллера MVC или страницы Razor. Приложения ASP.NET Core 5.0
могут передавать состояние в DynamicRouteValueTransformer и фильтровать
выбранный набор конечных точек.
Прочее
Этот атрибут [Compare] можно применить к свойствам в модели страницы
Razor.
Параметры и свойства, привязанные из текста, считаются обязательными по
умолчанию.
Веб-интерфейс API
Спецификация OpenAPI включена по умолчанию
Спецификация OpenAPI
является отраслевым стандартом для описания API-
интерфейсов HTTP и их интеграции в сложные бизнес-процессы или со
сторонними производителями. OpenAPI широко поддерживается всеми
поставщиками облачных служб и многими реестрами API. В приложениях,
создающих документы OpenAPI из веб-API, представлено множество новых
возможностей, в которых можно использовать эти API-интерфейсы. В условиях
партнерства с издателями проекта Swashbuckle.AspNetCore
с открытым кодом
шаблон API для ASP.NET Core содержит зависимость NuGet от Swashbuckle
.
Swashbuckle — это популярный пакет NuGet с открытым кодом, который
динамически создает документы OpenAPI. Он выполняет это путем самоанализа
через контроллеры API и создания документа OpenAPI во время выполнения или
во время сборки, используя интерфейс командной строки Swashbuckle.
В ASP.NET Core 5.0 шаблоны веб-API по умолчанию поддерживают OpenAPI. Чтобы
отключить OpenAPI:
Из командной строки.
Интерфейс командной строки.NET
dotnet new webapi --no-openapi true
Из Visual Studio: снимите флажок Enable OpenAPI support (Включить
поддержку OpenAPI).
Все файлы .csproj , созданные для проектов веб-API, содержат ссылку на пакет
NuGet Swashbuckle.AspNetCore
.
XML
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
</ItemGroup>
Код, созданный шаблоном, содержит код в Startup.ConfigureServices , который
активирует создание документа OpenAPI:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApp1", Version =
"v1" });
});
}
Метод Startup.Configure добавляет ПО промежуточного слоя Swashbuckle, которое
активирует:
процесс создания документа;
страницу пользовательского интерфейса Swagger по умолчанию в режиме
разработки.
Созданный шаблоном код не станет по случайности предоставлять описание API
при публикации в рабочей среде.
C#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
"WebApp1 v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Импорт Управления API Azure
Если проекты API для ASP.NET Core включают OpenAPI, Visual Studio 2019
версии 16.8 и более поздних публикаций автоматически предлагают
дополнительный этап в потоке публикации. Разработчики, использующие
Управление API Azure, имеют возможность автоматического импорта APIинтерфейсов в Управление API Azure во время потока публикации:
Улучшенный интерфейс запуска для проектов веб-API
Если OpenAPI включен по умолчанию, интерфейс запуска приложений (F5) для
разработчиков веб-API значительно улучшается. В ASP.NET Core 5.0 шаблон веб-API
предварительно настроен для загрузки страницы пользовательского интерфейса
Swagger. На странице пользовательского интерфейса Swagger содержится
документация, добавленная для опубликованного API, и обеспечивается
тестирование API-интерфейсов одним щелчком мыши.
Blazor
Повышение производительности
В .NET 5 мы внесли значительные улучшения в производительность среды
выполнения Blazor WebAssembly, делая основной акцент на сложной отрисовке
пользовательского интерфейса и сериализации JSON. По результатам наших тестов
производительности в большинстве сценариев скорость Blazor WebAssembly в
.NET 5 в два-три раза выше. Дополнительные сведения см. в блоге ASP.NET об
обновлениях ASP.NET Core в .NET 5 (релиз-кандидат 1) .
Изоляция CSS
Теперь Blazor поддерживает определение стилей CSS, областью действия которых
является данный компонент. Благодаря стилям CSS для отдельных компонентов
проще ориентироваться в стилях в приложении и избежать непредвиденных
побочных эффектов от применения глобальных стилей. Дополнительные сведения
см. в статье Изоляция CSS в ASP.NET Core Blazor.
Новый компонент InputFile
Компонент InputFile позволяет считывать один или несколько файлов, выбранных
пользователем для отправки. Дополнительные сведения см. в статье Отправка
файлов в ASP.NET Core Blazor.
Новые компоненты InputRadio и InputRadioGroup
Blazor содержит встроенные компоненты InputRadio и InputRadioGroup , которые
упрощают привязку данных к группам переключателей со встроенной проверкой.
Дополнительные сведения см. в статье Формы и компоненты ввода в Blazor для
ASP.NET Core.
Виртуализация компонентов
Повысьте воспринимаемую производительность отрисовки компонентов с
помощью встроенной поддержки виртуализации платформы Blazor.
Дополнительные сведения см. в статье Виртуализация компонентов Razor ASP.NET
Core.
Поддержка события ontoggle
События Blazor теперь поддерживают событие DOM ontoggle . Дополнительные
сведения см. в статье Обработка событий Blazor в ASP.NET Core.
Установка фокуса пользовательского интерфейса в
приложениях Blazor
Используйте удобный метод FocusAsync со ссылками на элемент, чтобы установить
фокус пользовательского интерфейса на этот элемент. Дополнительные сведения
см. в статье Обработка событий Blazor в ASP.NET Core.
Настраиваемые атрибуты класса проверки CSS
Настраиваемые атрибуты класса проверки CSS полезны при интеграции с
платформами CSS, такими как Bootstrap. Дополнительные сведения см. в статье
Формы и компоненты ввода в Blazor для ASP.NET Core.
Поддержка IAsyncDisposable
Теперь компоненты Razor поддерживают интерфейс IAsyncDisposable для
асинхронного выпуска выделенных ресурсов.
Изоляция JavaScript и ссылки на объекты
Blazor реализует изоляцию JavaScript в стандартных модулях JavaScript .
Дополнительные сведения см. в статье Вызов функций JavaScript из методов .NET в
ASP.NET Core Blazor.
Поддержка компонентами форм отображаемого
имени
Следующие встроенные компоненты поддерживают отображаемые имена с
помощью параметра DisplayName .
InputDate
InputNumber
InputSelect
Дополнительные сведения см. в статье Формы и компоненты ввода в Blazor для
ASP.NET Core.
Параметры маршрута catch-all
В компонентах поддерживаются параметры маршрута catch-all, которые
захватывают пути в нескольких папках. Дополнительные сведения см. в статье
Маршрутизация ASP.NET Core Blazor и навигация.
Усовершенствования отладки
В ASP.NET Core 5.0 улучшена отладка приложений Blazor WebAssembly. Кроме того,
сейчас отладка поддерживается в Visual Studio для Mac. Дополнительные сведения
см. в статье Отладка в ASP.NET CoreBlazor WebAssembly.
Microsoft Identity версии 2.0 и MSAL версии 2.0
Теперь для безопасности Blazor используется Microsoft Identity версии 2.0
(Microsoft.Identity.Web
и Microsoft.Identity.Web.UI ) и MSAL версии 2.0.
Дополнительные сведения см. в статье о безопасности Blazor и узле Identity.
Защищенное хранилище браузера для Blazor Server
Теперь приложения Blazor Server могут использовать встроенную поддержку для
хранения сведений о состоянии приложения в браузере, который уже защищен от
незаконного изменения благодаря защите данных в ASP.NET Core. Данные могут
храниться либо в локальном хранилище браузера, либо в хранилище сеансов.
Дополнительные сведения. см. в статье Управление состоянием ASP.NET Core
Blazor.
Предварительная отрисовка Blazor WebAssembly
Благодаря улучшенной интеграции компонентов в моделях размещения
приложения Blazor WebAssembly теперь могут предварительно отрисовывать
выходные данные на сервере.
Улучшенная обрезка и компоновка
Blazor WebAssembly выполняет обрезку и компоновку промежуточного языка (IL)
во время сборки, чтобы затем удалить ненужный IL из выходных сборок
приложения. В выпуске ASP.NET Core 5.0 Blazor WebAssembly выполняет
улучшенную обрезку с помощью дополнительных параметров конфигурации.
Дополнительные сведения см. в статьях Настройка средства обрезки для ASP.NET
Core Blazor и Параметры обрезки.
Анализатор совместимости с браузерами
Приложения Blazor WebAssembly предназначены для использования в полной
контактной зоне API .NET, но из-за ограничений песочницы браузера
поддерживаются не все API-интерфейсы .NET. При выполнении в WebAssembly
неподдерживаемые API-интерфейсы вызывают PlatformNotSupportedException.
Анализатор совместимости платформ предупреждает разработчика, когда
приложение использует API-интерфейсы, которые не поддерживаются целевыми
платформами приложения. Дополнительные сведения см. в статье Использование
компонентов в ASP.NET Core из библиотеки классов Razor (RCL).
Сборки с отложенной загрузкой
Производительность запуска приложения Blazor WebAssembly можно повысить,
отложив загрузку некоторых сборок приложений, пока они не потребуются.
Дополнительные сведения см. в статье Настройка средства обрезки для ASP.NET
Core Blazor WebAssembly.
Обновленная поддержка глобализации
Поддержка глобализации доступна для Blazor WebAssembly на основе ICU
(международных компонентов для Юникода). Дополнительные сведения см. в
статье Глобализация и локализация в ASP.NET Core Blazor.
gRPC
В gRPC
внесено много усовершенствований производительности.
Дополнительные сведения см. в статье Улучшения производительности gRPC в
.NET 5 .
Дополнительные сведения о gRPC см. в статье Общие сведения о gRPC на .NET.
SignalR
Фильтры концентраторов SignalR
Фильтры концентраторов SignalR, называемые конвейерами концентраторов в
ASP.NET SignalR, — это возможность, которая позволяет выполнять код до и после
вызова методов концентратора. Выполнение кода до и после вызова методов
концентратора аналогично тому, как ПО промежуточного слоя может выполнять
код до и после HTTP-запроса. Распространенные варианты использования
включают ведение журнала, обработку ошибок и проверку аргументов.
Подробные сведения см. в статье Использование фильтров концентраторов в
ASP.NET Core SignalR.
Параллельные вызовы концентраторов SignalR
ASP.NET Core SignalR теперь может обрабатывать параллельные вызовы
концентраторов. Поведение по умолчанию можно изменить, чтобы разрешить
клиентам одновременно вызывать несколько методов концентратора:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR(options =>
{
options.MaximumParallelInvocationsPerClient = 5;
});
}
Добавлена поддержка MessagePack в клиент Java для
SignalR
Новый пакет com.microsoft.signalr.messagepack
добавляет поддержку
MessagePack в клиент Java для SignalR. Чтобы использовать протокол
концентратора MessagePack, добавьте .withHubProtocol(new
MessagePackHubProtocol()) в построитель подключений:
Java
HubConnection hubConnection = HubConnectionBuilder.create(
"http://localhost:53353/MyHub")
.withHubProtocol(new MessagePackHubProtocol())
.build();
Kestrel
Перезагружаемые конечные точки с помощью конфигурации. Kestrel может
обнаружить изменения в конфигурации, переданные
KestrelServerOptions.Configure, отменить привязку к имеющимся конечным
точкам и привязать к новым конечным точкам, не требуя перезапуска
приложения, если для параметра reloadOnChange задано значение true . По
умолчанию при использовании ConfigureWebHostDefaults или
CreateDefaultBuilderKestrel привязывается к подразделу конфигурации
"Kestrel"
с включенным параметром reloadOnChange . Приложения должны
передавать reloadOnChange: true при вызове KestrelServerOptions.Configure
вручную для получения перезагружаемых конечных точек.
Улучшения заголовков ответов HTTP/2. Дополнительные сведения см. в
следующем разделе Улучшения в области производительности.
Поддержка дополнительных типов конечных точек в транспортировке
сокетов. В дополнение к новому API, представленному в System.Net.Sockets,
транспортировка сокетов по умолчанию в Kestrel позволяет привязаться как к
имеющимся дескрипторам файлов, так и к сокетам домена Unix. Поддержка
привязки к имеющимся дескрипторам файлов позволяет использовать
имеющуюся интеграцию Systemd , не требуя транспортировки libuv .
Декодирование настраиваемого заголовка в Kestrel. Приложения могут
указать, какие Encoding использовать для интерпретации входящих
заголовков на основе имени заголовка, а не по умолчанию в UTF-8. Задайте
свойство
Microsoft.AspNetCore.Server.Kestrel.KestrelServerOptions.RequestHeaderEncoding
Selector , чтобы указать используемую кодировку:
C#
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.RequestHeaderEncodingSelector = encoding =>
{
return encoding switch
{
"Host" => System.Text.Encoding.Latin1,
_ => System.Text.Encoding.UTF8,
};
};
});
webBuilder.UseStartup<Startup>();
});
Параметры для конечной точки Kestrel с помощью
конфигурации
Добавлена поддержка настройки параметров для каждой конечной точки Kestrel с
помощью конфигурации. Конфигурации для каждой конечной точки включают:
используемые протоколы HTTP;
используемые протоколы TLS;
выбранный сертификат;
режим сертификата клиента.
Конфигурация позволяет определить, какой сертификат выбирается на основе
указанного имени сервера. Имя сервера входит в состав расширения указания
имени сервера для протокола TLS, как указано клиентом. Конфигурация Kestrel
также поддерживает префикс подстановочного знака в имени узла.
В следующем примере показано, как задать для каждой конечной точки
использование файла конфигурации:
JSON
{
"Kestrel": {
"Endpoints": {
"EndpointName": {
"Url": "https://*",
"Sni": {
"a.example.org": {
"Protocols": "Http1AndHttp2",
"SslProtocols": [ "Tls11", "Tls12"],
"Certificate": {
"Path": "testCert.pfx",
"Password": "testPassword"
},
"ClientCertificateMode" : "NoCertificate"
},
"*.example.org": {
"Certificate": {
"Path": "testCert2.pfx",
"Password": "testPassword"
}
},
"*": {
// At least one sub-property needs to exist per
// SNI section or it cannot be discovered via
// IConfiguration
"Protocols": "Http1",
}
}
}
}
}
}
Указание имени сервера — это расширение TLS для включения виртуального
домена в состав согласования SSL. Это фактически означает, что виртуальное
доменное имя или имя узла можно использовать для определения конечной точки
сети.
Усовершенствования в области
производительности
HTTP/2
Значительное сокращение распределений в пути к коду HTTP/2.
Поддержка динамического сжатия HPack
заголовков ответов HTTP/2 в
Kestrel. Дополнительные сведения см. в разделе Размер таблицы заголовка и
записи блога HPACK: возможность Silent Killer в HTTP/2 .
Отправка кадров проверки связи HTTP/2. HTTP/2 имеет механизм отправки
кадров проверки связи, чтобы неактивное соединение по-прежнему
работало. Обеспечение работоспособного соединения особенно полезно при
работе с долгосрочными потоками, которые часто находятся в неактивном
состоянии, но только периодически видят действия, например потоки gRPC.
Приложения могут отправить периодические кадры проверки связи в Kestrel,
установив ограничения на KestrelServerOptions:
C#
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
options.Limits.Http2.KeepAlivePingInterval =
TimeSpan.FromSeconds(10);
options.Limits.Http2.KeepAlivePingTimeout =
TimeSpan.FromSeconds(1);
});
webBuilder.UseStartup<Startup>();
});
Контейнеры
До выпуска .NET 5.0 создание и публикация документа Dockerfile для приложения
ASP.NET Core, необходимого для получения всего пакета SDK для .NET Core и
образа ASP.NET Core. В этом выпуске количество извлекаемых байтов образов
пакета SDK сокращено, а байты, полученные для образа ASP.NET Core, как правило,
устраняются. Дополнительные сведения об этом см. в комментарии к проблеме
на GitHub .
Аутентификация и авторизация
Проверка подлинности Azure Active Directory с
помощью Microsoft.Identity.Web
Шаблоны проектов ASP.NET Core теперь интегрируются с Microsoft.Identity.Web для
управления проверкой подлинности с помощью Azure Activity Directory (Azure AD).
Пакет Microsoft.Identity.Web
предоставляет:
Улучшенный интерфейс проверки подлинности с помощью Azure AD.
Более простой способ доступа к ресурсам Azure от имени пользователей,
включая Microsoft Graph. См. пример Microsoft.Identity.Web , который
начинается с базового входа и продолжается через мультитенантность с
использованием API-интерфейсов Azure, Microsoft Graph и защиту
собственных API-интерфейсов. Пакет Microsoft.Identity.Web доступен вместе
с .NET 5.
Разрешение анонимного доступа к конечной точке
Метод расширения AllowAnonymous предоставляет анонимный доступ к конечной
точке:
C#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
})
.AllowAnonymous();
});
}
Пользовательская обработка сбоев авторизации
Пользовательская обработка сбоев авторизации стала еще проще благодаря
новому интерфейсу IAuthorizationMiddlewareResultHandler , который вызывается
ПО промежуточного слояавторизации. Реализация по умолчанию остается
неизменной, но пользовательский обработчик можно зарегистрировать во
внедрении зависимостей, что позволяет использовать пользовательские HTTP-
ответы в зависимости от причины сбоя авторизации. См. этот пример , в котором
демонстрируется использование IAuthorizationMiddlewareResultHandler .
Авторизация при использовании маршрутизации
конечных точек
Авторизация при использовании маршрутизации конечных точек теперь получает
HttpContext , а не экземпляр конечной точки. Это позволяет ПО промежуточного
слоя авторизации получить доступ к RouteData и другим свойствам HttpContext ,
которые были недоступны через класс Endpoint. Конечную точку можно получить
из контекста, используя context.GetEndpoint.
Управление доступом на основе ролей с помощью
проверки подлинности Kerberos и протокола LDAP в
Linux
См. раздел Проверка подлинности Kerberos и управление доступом на основе
ролей (RBAC).
Усовершенствования API
Методы расширения JSON для HttpRequest и
HttpResponse
Данные JSON можно считывать и записывать из HttpRequest и HttpResponse с
помощью новых методов расширения ReadFromJsonAsync и WriteAsJsonAsync . Эти
методы расширения используют сериализатор System.Text.Json для обработки
данных JSON. Новый метод расширения HasJsonContentType также может
проверять, имеет ли запрос тип содержимого JSON.
Методы расширения JSON можно объединять с маршрутизацией конечных точек
для создания API-интерфейсов JSON в стиле программирования, который мы
называем маршрут к коду. Это новый вариант для разработчиков, которые хотят
создавать базовые API-интерфейсы JSON простым способом. Например, вебприложение, имеющее только несколько конечных точек, может использовать
маршрут к коду, а не все функции MVC ASP.NET Core:
C#
endpoints.MapGet("/weather/{city:alpha}", async context =>
{
var city = (string)context.Request.RouteValues["city"];
var weather = GetFromDatabase(city);
await context.Response.WriteAsJsonAsync(weather);
});
System.Diagnostics.Activity
System.Diagnostics.Activity теперь по умолчанию имеет формат W3C. Благодаря
этому распределенная трассировка в ASP.NET Core поддерживает взаимодействие
с другими платформами по умолчанию.
FromBodyAttribute
FromBodyAttribute теперь поддерживает настройку параметра, который позволяет
считать такие параметры или свойства необязательными:
C#
public IActionResult Post([FromBody(EmptyBodyBehavior =
EmptyBodyBehavior.Allow)]
MyModel model)
{
...
}
Прочие улучшения
Мы начали применять заметки, допускающие значение NULL, к сборкам ASP.NET
Core. Мы планируем добавить заметки к большей части контактной зоны
общедоступного API на платформе .NET 5.
Управление активацией класса Startup
Добавлена дополнительная перегрузка UseStartup, которая разрешает
приложению предоставлять фабричный метод для управления активацией класса
Startup . Управление активацией класса Startup полезно для передачи
дополнительных параметров в Startup , которые инициализируются вместе с
узлом:
C#
public class Program
{
public static async Task Main(string[] args)
{
var logger = CreateLogger();
var host = Host.CreateDefaultBuilder()
.ConfigureWebHost(builder =>
{
builder.UseStartup(context => new Startup(logger));
})
.Build();
await host.RunAsync();
}
}
Автоматическое обновление с помощью dotnet watch
В .NET 5 при запуске dotnet watch в проекте ASP.NET Core запускается браузер по
умолчанию и выполняется автоматическое обновление браузера по мере
внесения изменений в код. Это означает, что вы можете:
Открыть проект ASP.NET Core в текстовом редакторе.
Выполните dotnet watch .
Сосредоточиться на изменениях кода, пока инструментарий обрабатывает
перестроение, перезапуск и перезагрузку приложения.
Форматировщик средства ведения журнала консоли
В библиотеке Microsoft.Extensions.Logging для поставщика журнала консоли
внесено несколько усовершенствований. Теперь разработчики могут реализовать
пользовательский ConsoleFormatter , чтобы полностью контролировать
форматирование и цветовое выделение выходных данных консоли. APIинтерфейсы форматировщика позволяют выполнять обширное форматирование
путем реализации подмножества escape-последовательностей VT-100. VT-100
поддерживается в большинстве современных терминалов. Средство ведения
журнала консоли может анализировать escape-последовательности
неподдерживаемых терминалов, позволяя разработчикам создавать единый
форматировщик для всех терминалов.
Средство ведения журнала консолиJSON
Помимо поддержки пользовательских форматировщиков, мы также добавили
встроенный форматировщик JSON, который выводит структурированные журналы
JSON на консоль. В следующем примере кода показано, как переключиться со
средства ведения журнала по умолчанию на JSON:
C#
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.AddJsonConsole(options =>
{
options.JsonWriterOptions = new JsonWriterOptions()
{ Indented = true };
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Сообщения журнала, выводимые на консоль, имеют формат JSON:
JSON
{
"EventId": 0,
"LogLevel": "Information",
"Category": "Microsoft.Hosting.Lifetime",
"Message": "Now listening on: https://localhost:5001",
"State": {
"Message": "Now listening on: https://localhost:5001",
"address": "https://localhost:5001",
"{OriginalFormat}": "Now listening on: {address}"
}
}
Новые возможности в ASP.NET
Core 3.1
Статья • 28.01.2023 • Чтение занимает 2 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 3.1 со
ссылками на соответствующую документацию.
Поддержка разделяемых классов для
компонентов Razor
Компоненты Razor теперь создаются как разделяемые классы. Код компонента
Razor можно написать как файл кода программной части, определенный как
разделяемый класс, вместо того чтобы определять весь код компонента в одном
файле. Дополнительные сведения см. в разделе Поддержка разделяемых классов.
Вспомогательная функция тега компонента и
передача параметров в компоненты
верхнего уровня
В Blazor с ASP.NET Core 3.0 компоненты отрисовывались как страницы и
представления с помощью вспомогательной функции HTML
( Html.RenderComponentAsync ). В ASP.NET Core 3.1 компонент отрисовывается из
страницы или представления с помощью новой вспомогательной функции тега
компонента:
CSHTML
<component type="typeof(Counter)" render-mode="ServerPrerendered" />
Вспомогательная функция HTML по-прежнему поддерживается в ASP.NET Core 3.1,
но рекомендуется использовать вспомогательную функцию тега компонента.
Приложения Blazor Server теперь могут передавать параметры в компоненты
верхнего уровня во время первоначальной отрисовки. Ранее параметры можно
было передавать в компонент верхнего уровня только с помощью
RenderMode.Static. В этом выпуске поддерживается как RenderMode.Server, так и
RenderMode.ServerPrerendered. Все указанные значения параметров сериализуются
в формат JSON и включаются в исходный ответ.
Например, компонент Counter может предварительно отрисовываться со
значением приращения ( IncrementAmount ):
CSHTML
<component type="typeof(Counter)" render-mode="ServerPrerendered"
param-IncrementAmount="10" />
Дополнительные сведения см. в разделе Интеграция компонентов в Razor Pages и
приложения MVC.
Поддержка общих очередей в HTTP.sys
HTTP.sys поддерживает создание анонимных очередей запросов. В ASP.NET
Core 3.1 добавлена возможность создания именованной очереди запросов
HTTP.sys или присоединения к существующей очереди. Создание именованной
очереди запросов HTTP.sys или присоединение к ней обеспечивает сценарии, в
которых процесс контроллера HTTP.Sys, которому принадлежит очередь, не
зависит от процесса прослушивателя. Такая независимость позволяет сохранять
существующие соединения и находящиеся в очереди запросы при перезапуске
процесса прослушивателя:
C#
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// ...
webBuilder.UseHttpSys(options =>
{
options.RequestQueueName = "MyExistingQueue";
options.RequestQueueMode = RequestQueueMode.CreateOrAttach;
});
});
Критические изменения для файлов cookie
SameSite
Поведение файлов cookie SameSite изменилось в соответствии с предстоящими
изменениями в браузерах. Это может повлиять на сценарии проверки
подлинности, такие как AzureAd, OpenIdConnect или WsFederation. Дополнительные
сведения см. в статье Работа с файлами cookie SameSite в ASP.NET Core.
Запрет выполнения действий по умолчанию
для событий в приложениях Blazor
Используйте атрибут директивы @on{EVENT}:preventDefault , чтобы предотвратить
выполнение действия по умолчанию для события. В следующем примере
запрещается действие по умолчанию, отображающее символ клавиши в текстовом
поле:
razor
<input value="@_count" @onkeypress="KeyHandler" @onkeypress:preventDefault
/>
Дополнительные сведения см. в разделе Запрет действий по умолчанию.
Остановка распространения событий в
приложениях Blazor
Используйте атрибут директивы @on{EVENT}:stopPropagation , чтобы остановить
распространение событий. В следующем примере установка флажка блокирует
передачу событий щелчка мышью из дочернего элемента <div> в родительский
элемент <div> .
razor
<input @bind="_stopPropagation" type="checkbox" />
<div @onclick="OnSelectParentDiv">
<div @onclick="OnSelectChildDiv"
@onclick:stopPropagation="_stopPropagation">
...
</div>
</div>
@code {
private bool _stopPropagation = false;
}
Дополнительные сведения см. в разделе Отключение распространения событий.
Подробные сведения об ошибках во время
разработки приложений Blazor
Если во время разработки приложение Blazor работает неправильно, подробные
сведения об ошибках в приложении могут помочь в устранении неполадок. При
возникновении ошибки в приложении Blazor в нижней части экрана отображается
золотистая полоска.
Во время разработки из этой полоски можно перейти в консоль браузера, где
можно просмотреть исключение.
В рабочей среде эта полоска уведомляет пользователя о том, что произошла
ошибка, и рекомендует обновить содержимое окна браузера.
Дополнительные сведения см. в статье Обработка ошибок в приложениях Blazor
ASP.NET Core.
Новые возможности в ASP.NET
Core 3.0
Статья • 28.01.2023 • Чтение занимает 14 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 3.0 со
ссылками на соответствующую документацию.
Blazor
Blazor — это новая платформа в ASP.NET Core, предназначенная для создания
интерактивного веб-интерфейса на стороне клиента с использованием .NET. Она
воплощает следующие возможности:
создание многофункциональных интерактивных пользовательских
интерфейсов на C# вместо JavaScript;
совместное использование серверной и клиентской логик приложений,
написанных с помощью .NET;
отображение пользовательского интерфейса в виде HTML-страницы с CSS для
широкой поддержки браузеров, в том числе для мобильных устройств.
Поддерживаемые сценарии платформы Blazor:
повторно используемые компоненты пользовательского интерфейса
(компоненты Razor);
маршрутизация на стороне клиента;
макеты компонентов;
поддержка внедрения зависимостей;
Формы и проверка
Предоставление компонентов Razor в библиотеках классов Razor
Взаимодействие с JavaScript
Дополнительные сведения см. в статье об ASP.NET Blazor.
Blazor Server
Blazor отделяет логику отображения компонентов от того, как применяются
обновления пользовательского интерфейса. Blazor Server предоставляет поддержку
размещения компонентов Razor на сервере в приложении ASP.NET Core.
Обновления пользовательского интерфейса передаются через подключение
SignalR. Blazor Server поддерживается только в ASP.NET Core 3.0.
Blazor WebAssembly (предварительная версия)
Приложения Blazor можно также запускать напрямую в браузере с
использованием среды выполнения .NET на основе WebAssembly. Платформа
Blazor WebAssembly доступна в режиме предварительной версии и не
поддерживается в ASP.NET Core 3.0. Blazor WebAssembly будет поддерживаться в
будущем выпуске ASP.NET Core.
составные части компонента Razor.
Приложения Blazor создаются на основе компонентов. Компоненты — это
автономные блоки пользовательского интерфейса, такие как страница, диалоговое
окно или форма. Это обычные классы .NET, определяющие логику отрисовки
пользовательского интерфейса и обработчики событий на стороне клиента.
Многофункциональные интерактивные веб-приложения можно создавать без
JavaScript.
Компоненты в Blazor обычно создаются с использованием синтаксиса Razor,
естественного сочетания HTML и C#. Компоненты Razor похожи на Razor Pages и
представления MVC тем, что они используют Razor. В отличие от страниц и
представлений, которые созданы на базе модели "запрос и ответ", компоненты
используются исключительно для обработки компоновки пользовательского
интерфейса.
gRPC
gRPC :
это популярная высокопроизводительная платформа RPC (удаленный вызов
процедур).
Для разработки API используется подход, при котором сначала создается
контракт.
Использует современные технологии, такие как:
HTTP/2 для транспортировки;
буферы протоколов в качестве языка описания интерфейса;
формат двоичной сериализации.
Предоставляет следующие возможности:
Проверка подлинности
двунаправленная потоковая передача и управление потоком;
отмена и время ожидания.
К функциям gRPC в ASP.NET Core 3.0 относятся:
Grpc.AspNetCore.
Платформа ASP.NET Core для размещения служб gRPC.
gRPC в ASP.NET Core поддерживает интеграцию со стандартными
возможностями ASP.NET Core, такими как ведение журнала, внедрение
зависимостей, проверка подлинности и авторизация.
Grpc.Net.Client.
Клиент gRPC для .NET Core, созданный на основе знакомого
клиента HttpClient .
Grpc.Net.ClientFactory.
Интеграция клиента gRPC с HttpClientFactory .
Дополнительные сведения см. в статье Общие сведения о gRPC на .NET.
SignalR
Инструкции по миграции см. в разделе об обновлении кода SignalR. Теперь SignalR
использует System.Text.Json для сериализации и десериализации сообщений JS.
Инструкции по восстановлению сериализатора на основе Newtonsoft.Json см. в
разделе Switch to Newtonsoft.Json (Переключение на Newtonsoft.JSON).
В клиентах JavaScript и .NET для SignalR добавлена поддержка автоматического
повторного подключения. По умолчанию клиент пытается немедленно заново
подключиться и повторить попытку через 2, 10 и 30 секунд при необходимости.
Если клиент успешно повторно подключается, он получает новый идентификатор
подключения. Автоматическое повторное подключение необходимо явно
выбирать:
JavaScript
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chathub")
.withAutomaticReconnect()
.build();
Интервалы повторного подключения можно указать, передав массив длительности
в миллисекундах:
JavaScript
.withAutomaticReconnect([0, 3000, 5000, 10000, 15000, 30000])
//.withAutomaticReconnect([0, 2000, 10000, 30000]) The default intervals.
Пользовательскую реализацию можно передать для полного управления
интервалами повторного подключения.
Если повторное подключение не удается выполнить после последнего интервала
повторного подключения:
клиент считает, что подключение находится в автономном режиме;
клиент прекращает попытки повторного подключения.
Во время повторных попыток подключения обновите пользовательский интерфейс
приложения, чтобы уведомить пользователя о попытке повторного подключения.
Чтобы обеспечить возможность отправлять отзывы о пользовательском
интерфейсе при прерывании подключения, в API клиента SignalR добавлены
следующие обработчики событий:
onreconnecting : дает разработчикам возможность отключить
пользовательский интерфейс или сообщить пользователям о том, что
приложение находится в автономном режиме.
onreconnected . Предоставляет разработчикам возможность обновлять
пользовательский интерфейс после повторного установления соединения.
Следующий код использует onreconnecting для обновления пользовательского
интерфейса при попытке подключения:
JavaScript
connection.onreconnecting((error) => {
const status = `Connection lost due to error "${error}". Reconnecting.`;
document.getElementById("messageInput").disabled = true;
document.getElementById("sendButton").disabled = true;
document.getElementById("connectionStatus").innerText = status;
});
Следующий код использует onreconnected для обновления пользовательского
интерфейса при подключении:
JavaScript
connection.onreconnected((connectionId) => {
const status = `Connection reestablished. Connected.`;
document.getElementById("messageInput").disabled = false;
document.getElementById("sendButton").disabled = false;
document.getElementById("connectionStatus").innerText = status;
});
SignalR 3.0 и более поздних версий предоставляется пользовательский ресурс для
обработчиков авторизации, когда методу концентратора требуется авторизация.
Ресурс является экземпляром HubInvocationContext . HubInvocationContext включает:
HubCallerContext
Имя вызываемого метода концентратора.
Аргументы для метода концентратора.
Рассмотрим следующий пример приложения чата, разрешающего вход нескольких
организаций с помощью Azure Active Directory. Любой пользователь с учетной
записью Майкрософт может войти в чат, но запрещать пользователям принимать
участие в обсуждениях или просматривать журналы чата пользователей могут
только члены владеющей организации. Приложение может ограничивать
определенные функции для определенных пользователей.
C#
public class DomainRestrictedRequirement :
AuthorizationHandler<DomainRestrictedRequirement, HubInvocationContext>,
IAuthorizationRequirement
{
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
DomainRestrictedRequirement requirement,
HubInvocationContext resource)
{
if (context.User?.Identity?.Name == null)
{
return Task.CompletedTask;
}
if (IsUserAllowedToDoThis(resource.HubMethodName,
context.User.Identity.Name))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool IsUserAllowedToDoThis(string hubMethodName, string
currentUsername)
{
if (hubMethodName.Equals("banUser",
StringComparison.OrdinalIgnoreCase))
{
return currentUsername.Equals("bob42@jabbr.net",
StringComparison.OrdinalIgnoreCase);
}
return currentUsername.EndsWith("@jabbr.net",
StringComparison.OrdinalIgnoreCase));
}
}
В приведенном выше коде DomainRestrictedRequirement служит пользовательским
IAuthorizationRequirement . Так как параметр ресурса HubInvocationContext
передается, внутренняя логика может выполнять следующее:
проверять контекст, в котором вызывается концентратор;
принимать решения о разрешении пользователю выполнять отдельные
методы концентратора.
Отдельные методы концентратора можно пометить именем политики, которую
проверяет код во время выполнения. Когда клиенты пытаются вызвать отдельные
методы концентратора, обработчик DomainRestrictedRequirement запускается и
управляет доступом к методам. В зависимости от того, как
DomainRestrictedRequirement управляет доступом:
все вошедшие в систему пользователи могут вызывать метод SendMessage ;
просматривать журналы пользователей могут только пользователи,
выполнившие вход с помощью адреса электронной почты @jabbr.net ;
запретить пользователям участвовать в обсуждениях могут только члены
bob42@jabbr.net .
C#
[Authorize]
public class ChatHub : Hub
{
public void SendMessage(string message)
{
}
[Authorize("DomainRestricted")]
public void BanUser(string username)
{
}
[Authorize("DomainRestricted")]
public void ViewUserHistory(string username)
{
}
}
Для создания политики DomainRestricted может потребоваться сделать следующее:
добавить новую политику в Startup.cs ;
предоставить настраиваемое требование DomainRestrictedRequirement в
качестве параметра;
зарегистрировать DomainRestricted с помощью ПО промежуточного слоя
авторизации.
C#
services
.AddAuthorization(options =>
{
options.AddPolicy("DomainRestricted", policy =>
{
policy.Requirements.Add(new DomainRestrictedRequirement());
});
});
Концентраторы SignalR используют маршрутизацию конечных точек. Подключение
концентратора SignalR было ранее выполнено явным образом:
C#
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("hubs/chat");
});
В предыдущей версии разработчикам требовалось подключать контроллеры,
страницы Razor и концентраторы в различных местах. В результате явного
подключения возникает последовательность практически идентичных сегментов
маршрутизации:
C#
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("hubs/chat");
});
app.UseRouting(routes =>
{
routes.MapRazorPages();
});
Концентраторы SignalR 3.0 можно маршрутизировать через конечные точки. При
такой маршрутизации, как правило, можно настроить всю маршрутизацию в
UseRouting .
C#
app.UseRouting(routes =>
{
routes.MapRazorPages();
routes.MapHub<ChatHub>("hubs/chat");
});
С SignalR ASP.NET Core 3.0 добавлены новые возможности.
потоковая передача между клиентом и сервером. При потоковой передаче между
клиентом и сервером методы на стороне сервера могут принимать экземпляры
IAsyncEnumerable<T> или ChannelReader<T> . В следующем примере C# метод
UploadStream в концентраторе получит поток строк от клиента:
C#
public async Task UploadStream(IAsyncEnumerable<string> stream)
{
await foreach (var item in stream)
{
// process content
}
}
Клиентские приложения .NET могут передавать экземпляр IAsyncEnumerable<T> или
ChannelReader<T> в качестве аргумента stream для метода концентратора
UploadStream , приведенного выше.
После завершения цикла for и завершения работы локальной функции
отправляется завершение потока:
C#
async IAsyncEnumerable<string> clientStreamData()
{
for (var i = 0; i < 5; i++)
{
var data = await FetchSomeData();
yield return data;
}
}
await connection.SendAsync("UploadStream", clientStreamData());
Клиентские приложения JavaScript используют SignalR Subject (или субъект RxJS )
для аргумента stream в приведенном выше методе UploadStream Hub.
JavaScript
let subject = new signalR.Subject();
await connection.send("StartStream", "MyAsciiArtStream", subject);
Код JavaScript может использовать метод subject.next для обработки строк по
мере их записи и готовности к отправке на сервер.
JavaScript
subject.next("example");
subject.complete();
С помощью кода, подобного двум предыдущим фрагментам, можно создать
потоковую передачу в реальном времени.
Новая сериализация JSON
ASP.NET Core 3.0 теперь по умолчанию использует System.Text.Json для
сериализации JSON:
асинхронно считывает и записывает JSON;
оптимизирован для текста UTF-8;
предоставляет более высокую производительность, чем Newtonsoft.Json .
Сведения о добавлении Json.NET в ASP.NET Core 3.0 см. в разделе Добавление
поддержки формата JSON на основе Newtonsoft.Json.
Новые директивы Razor
Следующий список содержит новые директивы Razor.
@attribute: Директива @attribute применяет этот атрибут к классу созданной
страницы или представления. Например, @attribute [Authorize] .
@implements: Директива @implements реализует интерфейс для созданного
класса. Например, @implements IDisposable .
IdentityServer4 поддерживает проверку
подлинности и авторизацию для веб-API и
одностраничных приложений
ASP.NET Core 3.0 обеспечивает проверку подлинности в одностраничных
приложениях с помощью поддержки авторизации веб-API. ASP.NET Core Identity
для проверки подлинности и хранения данных пользователей объединяется с
IdentityServer4
для реализации OpenID Connect.
IdentityServer4 — это платформа OpenID Connect и OAuth 2.0 для ASP.NET Core 3.0.
Она обеспечивает следующие функции безопасности:
Проверка подлинности как услуга (AaaS)
Единый вход (SSO) для нескольких типов приложений
Контроль доступа для API
Шлюз федерации
Дополнительные сведения см. в документации по IdentityServer4
или статье
Проверка подлинности и авторизация для одностраничных приложений.
Проверка подлинности Kerberos и проверка
подлинности с помощью сертификата
Для проверки подлинности с помощью сертификата требуется:
настройка сервера для принятия сертификатов;
добавление ПО промежуточного слоя для проверки подлинности в
Startup.Configure ;
добавление службы проверки подлинности с помощью сертификатов в
Startup.ConfigureServices .
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
// Other service configuration removed.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
// Other app configuration removed.
}
Параметры проверки подлинности с помощью сертификата включают следующие
возможности:
принятие самозаверяющих сертификатов;
проверка отзыва сертификатов;
проверка наличия в предложенном сертификате правильных флагов
использования.
Субъект-пользователь по умолчанию создается на основе свойств сертификата.
Субъект-пользователь содержит событие, которое позволяет дополнить или
заменить субъект. Дополнительные сведения см. в статье Настройка проверки
подлинности по сертификату в ASP.NET Core.
Проверка подлинности Windows теперь предусмотрена для Linux и macOS. В
предыдущих версиях аутентификация Windows была доступна только для IIS и
HTTP.sys. В ASP.NET Core 3.0 Kestrel может использовать Negotiate, Kerberos и NTLM
в Windows, Linux и macOS для узлов, присоединенных к домену Windows.
Поддержка в Kestrel этих схем проверки подлинности реализована в пакете
Microsoft.AspNetCore.Authentication.Negotiate NuGet . Как и в других службах
проверки подлинности, настройте приложение проверки подлинности во всей
организации, а затем настройте службу:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
// Other service configuration removed.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
// Other app configuration removed.
}
Требования к узлу:
На узлах Windows имена субъектов-служб должны быть добавлены в учетную
запись пользователя, где размещается приложение.
Компьютеры Linux и macOS должны быть присоединены к домену.
Имена субъектов-служб необходимо создать для веб-процесса.
Файлы Keytab необходимо создать и настроить на компьютере узла.
Дополнительные сведения см. в статье Настройка проверки подлинности Windows
в ASP.NET Core.
Изменения шаблонов
В шаблонах веб-интерфейса (Razor Pages, MVC с контроллером и представлениями)
удалены следующие элементы.
Пользовательского интерфейса согласия на файлы cookie больше нет.
Сведения о том, как включить функцию согласия для файлов cookie в
приложении, созданном на основе шаблона ASP.NET Core 3.0, см. в статье
Поддержка Общего регламента по защите данных (GDPR) в ASP.NET Core.
Для ссылки на скрипты и связанные статические ресурсы теперь используются
локальные файлы, а не CDN. Дополнительные сведения см. в статье В версии
3.0 для ссылки на скрипты и связанные статические ресурсы теперь
используются локальные файлы, а не CDN в зависимости от текущей среды
(dotnet/AspNetCore.Docs № 14350) .
Шаблон Angular обновлен для использования Angular 8.
По умолчанию для шаблона библиотеки классов Razor (RCL) используется
разработка компонентов Razor. Новый параметр шаблона в Visual Studio
обеспечивает поддержку шаблонов для страниц и представлений. При создании
RCL на основе шаблона в командной оболочке передайте параметр --supportpages-and-views ( dotnet new razorclasslib --support-pages-and-views ).
Универсальный узел
Шаблоны ASP.NET Core 3.0 используют универсальный узел .NET в ASP.NET Core. В
предыдущих версиях использовался WebHostBuilder. Использование
универсального узла .NET Core (HostBuilder) обеспечивает лучшую интеграцию
приложений ASP.NET Core с другими серверными сценариями, не зависящими от
Интернета. Дополнительные сведения см. в разделе HostBuilder replaces
WebHostBuilder (HostBuilder заменяет WebHostBuilder).
Конфигурация узла
До выхода ASP.NET Core 3.0 переменные среды с префиксом ASPNETCORE_ были
загружены для конфигурации веб-узла. В 3.0 AddEnvironmentVariables используется
для загрузки переменных среды с префиксом DOTNET_ для конфигурации узла с
помощью CreateDefaultBuilder .
Изменения во внедрении через конструктор Startup
Универсальный узел поддерживает только следующие типы для внедрения через
конструктор Startup :
IHostEnvironment
IWebHostEnvironment
IConfiguration
Все службы по-прежнему можно непосредственно внедрять в метод
Startup.Configure в качестве аргументов. Дополнительные сведения см. в статье
Generic Host restricts Startup constructor injection
(Универсальный узел
ограничивает внедрение через конструктор Startup) (№ 353).
Kestrel
В конфигурацию Kestrel добавлена возможность миграции на универсальный
узел. В версии 3.0 ConfigureWebHostDefaults настраивается в построителе вебузлов, предоставляемом Kestrel.
Адаптеры подключений удалены из Kestrel и заменены ПО промежуточного
слоя подключения, которое похоже на ПО промежуточного слоя HTTP в
конвейере ASP.NET Core, но для подключений более низкого уровня.
Транспортный уровень Kestrel предоставляется как открытый интерфейс в
Connections.Abstractions .
Неоднозначность заголовков и конечных строк устранена путем
перемещения конечных заголовков в новую коллекцию.
Из-за таких интерфейсов API с синхронными операциями ввода-вывода, как
HttpRequest.Body.Read , часто возникает нехватка потоков, что приводит к
сбоям приложений. В 3.0 AllowSynchronousIO отключен по умолчанию.
Дополнительные сведения см. в статье Миграция с ASP.NET Core 2.2 на 3.0.
HTTP/2 включено по умолчанию.
В Kestrel для конечных точек HTTPS по умолчанию включен протокол HTTP/2.
Поддержка HTTP/2 для IIS или HTTP.sys включена, если поддерживается
операционной системой.
EventCounters по запросу
EventSource размещения, Microsoft.AspNetCore.Hosting , выдает следующие новые
типы EventCounter, связанные с входящими запросами:
requests-per-second
total-requests
current-requests
failed-requests
Маршрутизация конечных точек
Маршрутизация конечных точек, позволяющая платформам (например, MVC)
хорошо работать с ПО промежуточного слоя, усовершенствована:
порядок ПО промежуточного слоя и конечных точек можно настроить в
конвейере обработки запросов Startup.Configure ;
конечные точки и ПО промежуточного слоя хорошо используются с другими
технологиями на основе ASP.NET Core, такими как проверки
работоспособности;
конечные точки могут реализовывать политику, например CORS или
авторизацию, в ПО промежуточного слоя и MVC;
фильтры и атрибуты можно разместить в методах контроллеров.
Дополнительные сведения см. в статье Маршрутизация в ASP.NET Core.
Проверки работоспособности
Для проверок работоспособности используется маршрутизация конечных точек с
универсальным узлом. В Startup.Configure вызовите MapHealthChecks для
построителя конечной точки с URL-адресом конечной точки или относительным
путем:
C#
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
Конечные точки проверки работоспособности могут:
указать один или несколько разрешенных узлов или портов;
требовать авторизацию;
требовать CORS.
Дополнительные сведения см. в следующих статьях:
Миграция с ASP.NET Core 2.2.x на 3.0
Проверки работоспособности в ASP.NET Core
Каналы в HttpContext
Теперь можно читать текст запроса и писать текст ответа с помощью API
System.IO.Pipelines. Свойство HttpRequest.BodyReader предоставляет класс
PipeReader, который можно использовать для чтения текста запроса. Свойство
HttpResponse.BodyWriter предоставляет класс PipeWriter, который можно
использовать для записи текста ответа. HttpRequest.BodyReader — это аналог потока
HttpRequest.Body . HttpResponse.BodyWriter — это аналог потока HttpResponse.Body .
Улучшены сообщения об ошибках в IIS
Теперь при ошибках запуска во время размещения приложений ASP.NET Core в IIS
предоставляются более полные диагностические данные. Сведения об этих
ошибках передаются в журнал событий Windows с трассировками стека везде, где
это применимо. Кроме того, все предупреждения, ошибки и необработанные
исключения записываются в журнал событий Windows.
Служба рабочих ролей и пакет SDK для
рабочей роли
В .NET Core 3.0 появился новый шаблон приложения службы рабочих ролей. Этот
шаблон может служить отправной точкой для написания длительных приложений
служб в .NET Core.
Дополнительные сведения можно найти в разделе
.NET Core Workers as Windows Services
(Рабочие роли .NET Core в качестве
служб Windows)
Фоновые задачи с размещенными службами в ASP.NET Core
Размещение ASP.NET Core в службе Windows
Улучшения ПО промежуточного слоя
перенаправления заголовков
В предыдущих версиях ASP.NET Core вызвать UseHsts и UseHttpsRedirection было
проблематично при развертывании в Azure Linux или за любым обратным проксисервером, отличным от IIS. Исправление для предыдущих версий описано в
разделе Переадресация схемы для Linux и обратных прокси-серверов не IIS.
Этот сценарий исправлен в ASP.NET Core 3.0. Узел включает ПО промежуточного
слоя перенаправления заголовков, если переменной среды
ASPNETCORE_FORWARDEDHEADERS_ENABLED присвоено значение true . В образах
контейнера для ASPNETCORE_FORWARDEDHEADERS_ENABLED задано значение true .
Улучшения производительности
В ASP.NET Core 3.0 реализованы многочисленные улучшения, сокращающие
использование памяти и повышающие пропускную способность:
сокращение использования памяти при использовании встроенного
контейнера внедрения зависимостей для служб с заданной областью;
сокращение количества распределений на платформе, включая сценарии ПО
промежуточного слоя и маршрутизацию;
сокращение использования памяти для подключений WebSocket;
сокращение использования памяти и улучшение пропускной способности для
подключений по протоколу HTTPS;
новый оптимизированный и полностью асинхронный сериализатор JSON;
сокращение использования памяти и улучшения пропускной способности при
анализе формы.
ASP.NET Core 3.0 работает только в .NET
Core 3.0
Начиная с ASP.NET Core 3.0, .NET Framework больше не является поддерживаемой
целевой платформой. Проекты, предназначенные для .NET Framework, можно
полноценно использовать с помощью выпуска LTS .NET Core 2.1 . Большинство
пакетов, связанных с ASP.NET Core 2.1.x, будут поддерживаться неограниченно
после истечения трехлетнего периода LTS для .NET Core 2.1.
Сведения о миграции см. в статье Перенос кода в .NET Core из .NET Framework.
Использование общей платформы .NET Core
Для общей платформы ASP.NET Core 3.0, содержащейся в метапакете
Microsoft.AspNetCore.app, больше не требуется явный элемент <PackageReference />
в файле проекта. При использовании пакета SDK Microsoft.NET.Sdk.Web в файле
проекта автоматически создается ссылка на общую платформу:
XML
<Project Sdk="Microsoft.NET.Sdk.Web">
Сборки, удаленные из общей платформы
ASP.NET Core
Наиболее важные сборки, удаленные из общей платформы ASP.NET Core 3.0:
Newtonsoft.Json
(Json.NET). Сведения о добавлении Json.NET в ASP.NET
Core 3.0 см. в разделе Добавление поддержки формата JSON на основе
Newtonsoft.Json. В ASP.NET Core 3.0 внедрен System.Text.Json для чтения
формата JSON и записи в него. Дополнительные сведения см. в разделе Новая
сериализация JSON в этом документе.
Entity Framework Core
Полный список сборок, удаленных из общей платформы, см. в разделе Assemblies
being removed from Microsoft.AspNetCore.App 3.0
(Сборки, удаленные из
Microsoft.AspNetCore.App 3.0). Дополнительные сведения о мотивации для этого
изменения см. в статье Breaking changes to Microsoft.AspNetCore.App in 3.0
(Критические изменения в Microsoft.AspNetCore.App 3.0) и записи блога A first look
at changes coming in ASP.NET Core 3.0
ASP.NET Core 3.0).
(Первое знакомство с изменениями в
Новые возможности ASP.NET Core 2.2
Статья • 28.01.2023 • Чтение занимает 4 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 2.2 со
ссылками на соответствующую документацию.
Соглашения и анализаторы OpenAPI
OpenAPI (ранее Swagger) — это не зависящая от языка спецификация для описания
REST API. В экосистеме OpenAPI представлены средства для обнаружения,
тестирования и создания кода клиента в соответствии со спецификацией.
Поддержка создания и визуализации документов OpenAPI в ASP.NET Core MVC
обеспечивается за счет управляемых сообществом проектов, таких как NSwag
и
Swashbuckle.AspNetCore . В ASP.NET Core 2.2 представлены улучшенные средства
и возможности среды выполнения для создания документов OpenAPI.
Дополнительные сведения см. в следующих ресурсах:
Использование анализаторов веб-API
Использование соглашений веб-API
ASP.NET Core 2.2.0, предварительная версия 1. Соглашения и анализаторы
OpenAPI
Поддержка сведений о проблеме
В ASP.NET Core 2.1 появился объект ProblemDetails , основанный на спецификации
RFC 7807
и предназначенный для передачи сведений об ошибке в составе HTTP-
ответа. В версии 2.2 ProblemDetails является стандартным ответом для кодов
ошибок клиента в контроллерах с атрибутами ApiControllerAttribute .
IActionResult , возвращающий код состояния ошибки клиента (4xx), теперь
возвращает текст ProblemDetails . В результат также включается идентификатор
корреляции, который может использоваться для сопоставления ошибки с
помощью журналов запросов. В отношении ошибок клиентов ProducesResponseType
по умолчанию использует тип ответа ProblemDetails . Это описывается в выходных
данных Open API (Swagger), создаваемых с помощью NSwag или
Swashbuckle.AspNetCore.
Маршрутизация конечных точек
В ASP.NET Core 2.2 используется новая система маршрутизации конечных точек,
обеспечивающая оптимизированную диспетчеризацию запросов. Среди
изменений представлены новые члены для создания ссылок API и
преобразователи параметров маршрута.
Дополнительные сведения см. в следующих ресурсах:
Маршрутизация конечных точек в версии 2.2
Преобразователи параметров маршрута
(см. раздел Маршрутизация)
Различия между IRouter и маршрутизацией на основе конечных точек
Проверки работоспособности
Новая служба проверки работоспособности упрощает использование ASP.NET Core
в средах с обязательной проверкой работоспособности, таких как Kubernetes.
Служба проверки работоспособности включает ПО промежуточного слоя и набор
библиотек, которые определяют абстракцию IHealthCheck и службу.
Проверки работоспособности используются подсистемой балансировки нагрузки
или оркестратором контейнеров для быстрого определения того, насколько
эффективно система отвечает на запросы. Оркестратор контейнеров может
реагировать на неуспешную проверку работоспособности, остановив
последовательное развертывание или перезапустив контейнер. Подсистема
балансировки нагрузки может реагировать на результаты проверки
работоспособности путем перенаправления трафика от неисправного экземпляра
службы.
Проверки работоспособности предоставляются приложением в качестве конечной
точки HTTP, используемой системами мониторинга. Проверки работоспособности
можно настроить для различных сценариев мониторинга в реальном времени и
систем мониторинга. Проверки работоспособности интегрируются с проектом
BeatPulse . Это значительно упрощает добавление проверок в десятки
распространенных систем и зависимых компонентов.
Дополнительные сведения см. в статье Проверки работоспособности в ASP.NET
Core.
HTTP/2 в Kestrel
В ASP.NET Core 2.2 добавлена поддержка HTTP/2.
HTTP/2 является основной редакцией HTTP-протокола. В число важных функций
HTTP/2 входят следующие:
Поддержка сжатия заголовка.
Полностью мультиплексные потоки через одно соединение.
Версия HTTP/2 сохраняет семантику HTTP (например, методы и заголовки HTTP),
однако она заметно отличается от HTTP/1.x в отношении механизмов
кадрирования и передачи данных между клиентом и сервером.
В связи с этим изменением механизмов кадрирования серверам и клиентам
необходимо согласовывать используемую версию протокола. Согласование
протоколов на уровне приложений (Application-Layer Protocol Negotiation, ALPN) —
это расширение TLS, с помощью которого сервер и клиент могут согласовать
версию протокола в рамках процесса подтверждения TLS. Даже при наличии у
сервера и клиента сведений о версии протокола все основные браузеры
поддерживают ALPN как единственное средство для установления соединения по
протоколу HTTP/2.
Дополнительные сведения см. в статье Поддержка HTTP/2.
Конфигурация Kestrel
В предыдущих версиях ASP.NET Core настройка параметров Kestrel осуществлялась
путем вызова UseKestrel . В версии 2.2 для настройки параметров Kestrel
вызывается метод ConfigureKestrel в построителе узла. Это изменение позволяет
устранить проблему с порядком регистрации IServer для внутрипроцессного
размещения. Дополнительные сведения см. в следующих ресурсах:
Устранение конфликта UseIIS
Настройка параметров сервера Kestrel путем настройки Kestrel
Внутрипроцессное размещение в службах
IIS
В более ранних версиях ASP.NET Core службы IIS выступают в качестве обратного
прокси-сервера. В версии 2.2 модуль ASP.NET Core может загружать CoreCLR и
размещать приложение в рабочем процессе IIS (w3wp.exe). Внутрипроцессное
размещение позволяет оптимизировать производительность и диагностику при
работе со службами IIS.
Дополнительные сведения см. в статье Внутрипроцессное размещение для служб
IIS.
Клиент Java SignalR
В ASP.NET Core 2.2 представлен клиент Java для SignalR. Этот клиент поддерживает
подключение к серверу SignalR ASP.NET Core из кода Java, в том числе из
приложений Android.
Дополнительные сведения см. в статье о клиенте Java для SignalR ASP.NET Core.
Усовершенствования CORS
В предыдущих версия ASP.NET Core ПО промежуточного слоя CORS обеспечивало
отправку заголовков Accept , Accept-Language , Content-Language и Origin
независимо от значений, настроенных в CorsPolicy.Headers . В версии 2.2
соблюдение политики ПО промежуточного слоя CORS возможно только в том
случае, если отправляемые в Access-Control-Request-Headers заголовки точно
соответствуют заголовкам, указанным в WithHeaders .
Дополнительные сведения см. в статье ПО промежуточного слоя CORS.
Сжатие ответов
ASP.NET Core 2.2 поддерживает сжатие ответов с использованием формата сжатия
Brotli .
Дополнительные сведения см. в статье Поддержка сжатия Brotli для сжатия ответов
в ПО промежуточного слоя.
Шаблоны проектов
Шаблоны веб-проектов ASP.NET Core обновлены до Bootstrap 4
и Angular 6
.
Новое оформление выглядит проще и позволяет более эффективно отображать
важные структуры приложения.
Производительность проверок
Модель MVC имеет расширяемую гибкую систему проверки, позволяющую на
основе запросов определять, какие средства проверки применяются к конкретной
модели. Эта возможность отлично подходит для разработки сложных поставщиков
служб проверки. Тем не менее в большинстве распространенных случаев
приложение использует только встроенные средства проверки и не нуждается в
таких дополнительных возможностях. К встроенным средствам проверки относятся
заметки к данным, такие как [Required] и [StringLength], а также IValidatableObject .
В ASP.NET Core 2.2 модель MVC может обойти проверку в случаях, когда
определяется, что для указанного графа модели проверка не требуется. Пропуск
проверки приводит к существенным улучшениям для моделей, в которых
отсутствуют или не могут использоваться средства проверки. К ним относятся такие
объекты, как коллекции примитивов (например, byte[] , string[] ,
Dictionary<string, string> ) или сложные графы объектов с небольшим
количеством средств проверки.
Производительность клиента HTTP
В ASP.NET Core 2.2 была повышена производительность SocketsHttpHandler за счет
уменьшения числа конфликтов, связанных с блокировкой пула подключений. Была
увеличена пропускная способность приложений, которые выполняют множество
исходящих запросов HTTP, таких как некоторые архитектуры микрослужб. Под
нагрузкой пропускная способность HttpClient может повышаться до 60 % в Linux и
до 20 % в Windows.
Дополнительные сведения см. в статье, посвященной запросам на вытягивание,
благодаря которым удалось добиться этого улучшения
.
Дополнительные сведения
Полный список изменений см. в статье Заметки о выпуске ASP.NET Core 2.2 .
Новые возможности ASP.NET Core 2.1
Статья • 28.01.2023 • Чтение занимает 5 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 2.1 со
ссылками на соответствующую документацию.
SignalR
SignalR переписан для ASP.NET Core 2.1.
В ASP.NET Core SignalR внесен ряд усовершенствований:
Упрощенная модель горизонтального масштабирования.
Новый клиент JavaScript без зависимости jQuery.
Новый компактный двоичный протокол на базе MessagePack.
Поддержка пользовательских протоколов.
Новая модель потокового ответа.
Поддержка клиентов на базе только протокола WebSocket.
Дополнительные сведения см. в статье об ASP.NET SignalR.
Библиотеки класса Razor
В ASP.NET Core 2.1 проще создать и включить пользовательский интерфейс на
основе Razor в библиотеку, а затем использовать его сразу в нескольких проектах.
Новый пакет SDK для Razor позволяет создавать файлы Razor в проекте библиотеки
классов, который можно поместить в пакет NuGet. Представления и страницы в
библиотеках обнаруживаются автоматически и могут переопределяться
приложением. Благодаря интеграции компиляции Razor в сборку:
Время запуска приложения значительно сократилось.
Быстрые обновления для представлений и страниц Razor во время
выполнения по-прежнему доступны в рамках рабочего процесса
последовательной разработки.
Дополнительные сведения см. в разделе Создание многоразового
пользовательского интерфейса с помощью проекта библиотеки классов Razor.
Библиотека пользовательского интерфейса
Identity и формирование шаблонов
ASP.NET Core 2.1 предоставляет ASP.NET Core Identity как библиотеку классов Razor.
Приложения, включающие Identity, могут применить новый шаблон Identity для
выборочного добавления исходного кода из библиотеки классов Identity (RCL) для
Razor. Вы можете создать исходный код, чтобы изменить код и тем самым
изменить поведение. Например, вы можете указать шаблону создать код,
используемый при регистрации. Созданный код имеет приоритет над тем же
кодом в RCL для Identity.
Приложения, которые не включают проверку подлинности, могут применить
шаблон Identity, чтобы добавить пакет RCL для Identity. Вы можете выбрать, какой
код Identity будет создан.
Дополнительные сведения см. в разделе Шаблоны Identity в проектах ASP.NET Core.
HTTPS
Учитывая повышенное внимание к безопасности и конфиденциальности, важно
использовать HTTPS для веб-приложений. В Интернете применение HTTPS все
чаще становится обязательным. Сайты, не использующие HTTPS, считаются
небезопасными. Браузеры (Chromium, Mozilla) требуют использования вебкомпонентов в контексте безопасности. Общий регламент по защите данных
предписывает использовать HTTPS для защиты конфиденциальности
пользователей. В то время как использование HTTPS в рабочей среде становится
обязательным, применение HTTPS при разработке помогает предотвратить
проблемы с развертыванием (например, небезопасные ссылки). ASP.NET Core 2.1
включает ряд усовершенствований, упрощающих использование HTTPS в среде
разработки и настройку HTTPS в рабочей среде. Дополнительные сведения см. в
разделе Обязательное использование HTTPS.
По умолчанию включено
Чтобы вам было проще разрабатывать безопасные веб-сайты, протокол HTTPS
теперь включен по умолчанию. Начиная с версии 2.1, Kestrel ожидает передачи
данных по адресу https://localhost:5001 , если присутствует локальный сертификат
разработки. Сертификат разработки создается, когда:
Вы впервые запускаете пакет SDK для .NET Core.
Вы создаете его вручную с помощью нового средства dev-certs .
Запустите dotnet dev-certs https --trust , чтобы установить доверие для
сертификата.
Перенаправление и принудительное применение
HTTPS
Веб-приложения обычно используют оба протокола — HTTP и HTTPS, но
перенаправляют весь трафик HTTP на HTTPS. В версии 2.1 появилось специальное
ПО промежуточного слоя, которое интеллектуально перенаправляет трафик на
HTTPS, учитывая конфигурацию или порты связанного сервера.
Использование протокола HTTPS также можно гарантировать с помощью
протокола HTTP Strict Transport Security (HSTS). HSTS указывает браузерам всегда
переходить на сайт через HTTPS. В ASP.NET Core 2.1 добавлено ПО
промежуточного слоя HSTS, которое поддерживает параметры для максимального
возраста, дочерних доменов и списка предварительной загрузки HSTS.
Конфигурация для рабочей среды
В рабочей среде необходимо явно настроить HTTPS. В версии 2.1 добавлена схема
конфигурации по умолчанию для настройки HTTPS для Kestrel. Можно настроить
приложения, чтобы они использовали:
Несколько конечных точек, включая URL-адреса. Дополнительные сведения
см. в статье Реализация веб-сервера Kestrel: конфигурация конечных точек.
Сертификат для использования HTTPS из файла на диске или из хранилища
сертификатов.
Общий регламент по защите данных
ASP.NET Core предоставляет API-интерфейсы и шаблоны, которые помогают
соответствовать требованиям Общего регламента по защите данных в ЕС .
Дополнительные сведения см. в разделе Поддержка общего регламента по защите
данных в ASP.NET Core. В примере приложения
показано, как использовать и
тестировать большинство точек расширения для общего регламента по защите
данных и API-интерфейсов, добавленных в шаблоны ASP.NET Core 2.1.
Интеграционные тесты
Добавлен новый пакет, который оптимизирует создание и выполнение тестов.
Пакет Microsoft.AspNetCore.Mvc.Testing
выполняет следующие задачи:
Копирует файл зависимостей (*.deps) из протестированного приложения в
папку bin тестового проекта.
Задает корневую папку содержимого в корневой папке проекта тестируемого
приложения, чтобы можно было найти статические файлы и страницы или
представления при выполнении тестов.
Предоставляет класс WebApplicationFactory<TEntryPoint> для оптимизации
начальной загрузки тестируемого приложения на TestServer.
В следующем тесте используется xUnit
для проверки того, что страница индексов
загружается с кодом состояния успешного выполнения и правильным заголовком
Content-Type:
C#
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly HttpClient _client;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup>
factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetHomePage()
{
// Act
var response = await _client.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Дополнительные сведения см. в разделе Интеграционные тесты.
[ApiController], ActionResult<T>
В ASP.NET Core 2.1 добавлены новые соглашения программирования, с которыми
проще создавать чистые и выразительные веб-API. ActionResult<T> — это новый
тип, который разрешает приложениям возвращать либо тип ответа, либо любой
другой результат действия (аналогично IActionResult), при этом по-прежнему
указывая тип ответа. Также был добавлен атрибут [ApiController] , с помощью
которого можно принять соглашения и поведения для веб-API.
Дополнительные сведения см. в разделе Сборка веб-API с использованием ASP.NET
Core.
IHttpClientFactory
ASP.NET Core 2.1 содержит новую службу IHttpClientFactory , которая упрощает
настройку и использование экземпляров HttpClient в приложениях. В HttpClient
уже существует концепция делегирования обработчиков, которые можно связать
друг с другом для исходящих HTTP-запросов. Фабрика:
Упрощает регистрацию экземпляров HttpClient каждого именованного
клиента.
Реализует обработчик Polly, который позволяет использовать политики Polly
для повтора, размыкателя цепи и т. д.
Дополнительные сведения см. в разделе Инициирование HTTP-запросов.
Конфигурация транспорта Kestrel Libuv.
После выпуска ASP.NET Core 2.1 транспорт Kestrel по умолчанию основан не на
Libuv, а на управляемых сокетах. Дополнительные сведения см. в статье Реализация
веб-сервера Kestrel: конфигурация транспорта Libuv.
Построитель универсальных узлов
Добавлен построитель универсальных узлов ( HostBuilder ). Построитель можно
использовать для приложений, которые не обрабатывают HTTP-запросы (обмен
сообщениями, фоновые задачи и т. д.).
Дополнительные сведения см. в разделе Универсальный узел .NET.
Обновленные шаблоны SPA
Обновлены шаблоны одностраничных приложений для Angular, React и React с
Redux. Теперь можно использовать стандартные структуры проектов и создавать
системы для каждой платформы.
Шаблон Angular основан на Angular CLI, а шаблоны React основаны на create-reactapp.
Дополнительные сведения можно найти в разделе
Использование Angular с ASP.NET Core
Использование React с ASP.NET Core
Использование шаблона проекта React и Redux с ASP.NET Core
Поиск в Razor Pages активов Razor
В версии 2.1 Razor Pages ищет ресурсы Razor (например, макеты и частично
выполненные строки) в следующих каталогах в указанном порядке.
1. Текущая папка Pages.
2. /Pages/Shared/
3. /Views/Shared/
Razor Pages в области
Razor Pages теперь поддерживает области. Чтобы увидеть пример областей,
создайте новое веб-приложение Razor Pages с отдельными учетными записями
пользователей. Веб-приложение Razor Pages с отдельными учетными записями
пользователей включает /Areas/Identity/Pages.
Совместимая версия MVC
Метод SetCompatibilityVersion позволяет приложению принимать или отклонять
потенциально критические изменения в поведении, появившиеся в ASP.NET Core
MVC 2.1 или более поздних версий.
Дополнительные сведения см. в статье Совместимая версия для ASP.NET Core MVC.
Миграция с 2.0 на 2.1
См. раздел Миграция с ASP.NET Core 2.0 на 2.1.
Дополнительные сведения
Полный список изменений см. в статье Заметки о выпуске ASP.NET Core 2.1 .
Новые возможности ASP.NET Core 2.0
Статья • 10.01.2023 • Чтение занимает 5 мин
В этой статье описываются наиболее важные изменения в ASP.NET Core 2.0 со
ссылками на соответствующую документацию.
Razor Pages
Razor Pages — это новая функция платформы MVC ASP.NET Core, которая делает
создание кодов сценариев для страниц проще и эффективнее.
Дополнительные сведения см. в следующей вводной статье и учебнике.
Введение в Razor Pages
Начало работы с Razor Pages
Метапакет ASP.NET Core
Новый метапакет ASP.NET Core включает все пакеты, выпущенные и
поддерживаемые командами ASP.NET Core и Entity Framework Core, а также
внутренние и сторонние зависимости. Вам больше не придется выбирать
отдельный пакет компонентов ASP.NET Core. Все компоненты входят в пакет
Microsoft.AspNetCore.All . Шаблоны по умолчанию используют именно этот пакет.
Дополнительные сведения см. в статье Метапакет Microsoft.AspNetCore.All для
ASP.NET Core 2.0.
Хранилище среды выполнения
Приложения, использующие метапакет Microsoft.AspNetCore.All , автоматически
получают все преимущества нового хранилища среды выполнения .NET Core.
Хранилище содержит все ресурсы среды выполнения, необходимые для запуска
приложений ASP.NET Core 2.0. При использовании метапакета
Microsoft.AspNetCore.All приложение не развертывает никакие ресурсы из
указанных по ссылке пакетов NuGet ASP.NET Core, так как эти пакеты уже
присутствуют в целевой системе. Кроме того, для сокращения времени запуска
приложения ресурсы в хранилище среды выполнения подвергаются
предварительной компиляции.
Дополнительные сведения см. в статье Хранилище среды выполнения.
.NET Standard 2.0
Пакеты ASP.NET 2.0 предназначены для .NET Standard 2.0. На эти пакеты могут
ссылаться другие библиотеки .NET Standard 2.0; кроме того, они могут выполняться
в реализациях .NET, совместимых с .NET Standard 2.0, включая .NET Core 2.0 и .NET
Framework 4.6.1.
Метапакет Microsoft.AspNetCore.All работает только с .NET Core 2.0, так как
предназначен для использования с хранилищем среды выполнения .NET Core 2.0.
Изменения в конфигурации
В ASP.NET Core 2.0 экземпляр IConfiguration добавляется в контейнер служб по
умолчанию. IConfiguration в контейнере служб упрощает для приложений задачу
получения значений конфигурации из контейнера.
Сведения о состоянии плановой документации см. в статье о проблемах GitHub .
Изменения в ведении журналов
В ASP.NET 2.0 Core ведение журнала по умолчанию включено в систему внедрения
зависимостей. Добавить поставщиков и настроить фильтрацию можно в файле
Program.cs , а не файле Startup.cs . А ILoggerFactory по умолчанию поддерживает
такой способ фильтрации, который позволяет использовать один гибкий подход и
для перекрестной фильтрации по поставщикам, и для фильтрации по отдельному
поставщику.
Дополнительные сведения см. в статье Введение в ведение журналов.
Изменения в проверке подлинности
Новая модель проверки подлинности облегчает настройку проверки подлинности
для приложения с использованием внедрения зависимостей.
Доступны новые шаблоны для настройки проверки подлинности в вебприложениях и веб-API с использованием Azure AD B2C
.
Сведения о состоянии плановой документации см. в статье о проблемах GitHub .
Обновление Identity
Мы упростили сборку защищенных веб-API с использованием Identity в
ASP.NET Core 2.0. Теперь маркеры доступа для обращения к веб-API можно
получить с помощью библиотеки проверки подлинности Microsoft (MSAL) .
Дополнительные сведения об изменениях в проверке подлинности в версии 2.0 см.
в следующих ресурсах.
Подтверждение учетной записи и восстановление пароля в ASP.NET Core
Включение создания QR-кодов для приложений проверки подлинности в
ASP.NET Core
Миграция проверки подлинности и в ASP.NET Core 2.0
Шаблоны SPA
Доступны шаблоны проектов одностраничных приложений (Single Page Application,
SPA) Angular, Aurelia, Knockout.js, React.js и React.js с Redux. Шаблон Angular
обновлен до Angular 4. Шаблоны Angular и React доступны по умолчанию.
Сведения о получении других шаблонов см. в разделе Создание проекта SPA.
Сведения о сборке SPA в ASP.NET Core см. в статье Создание одностраничных
приложений в ASP.NET Core с помощью служб JavaScript.
Улучшения Kestrel
В веб-сервере Kestrel реализованы новые функции, необходимые серверу с
выходом в Интернет. Добавлено несколько параметров конфигурации
ограничений для сервера в новое свойство Limits класса KestrelServerOptions . Вы
можете добавлять следующие ограничения:
максимальное число клиентских подключений;
максимальный размер текста запроса;
минимальная скорость передачи данных в тексте запроса.
Дополнительные сведения см. в статье Реализация веб-сервера в ASP.NET Core.
WebListener переименован в HTTP.sys
Пакеты Microsoft.AspNetCore.Server.WebListener и Microsoft.Net.Http.Server
объединены в новый пакет Microsoft.AspNetCore.Server.HttpSys . Соответственно
обновлены и пространства имен.
Дополнительные сведения см. в статье Реализация веб-сервера HTTP.sys в ASP.NET
Core.
Расширенная поддержка заголовков HTTP
Теперь при использовании MVC для передачи FileStreamResult или
FileContentResult можно указывать дату ETag или LastModified для передаваемого
содержимого. Для возвращаемого содержимого эти значения можно задать,
используя следующий код:
C#
var data = Encoding.UTF8.GetBytes("This is a sample text from a binary
array");
var entityTag = new EntityTagHeaderValue("\"MyCalculatedEtagValue\"");
return File(data, "text/plain", "downloadName.txt", lastModified:
DateTime.UtcNow.AddSeconds(-5), entityTag: entityTag);
Файл, возвращаемый вашим посетителям, будет иметь соответствующие заголовки
HTTP для значений ETag и LastModified .
Если посетители вашего приложение запросят содержимое с заголовком типа
"Запрос диапазона", ASP.NET Core распознает и обработает этот заголовок.
Запрошенное содержимое может доставляться частично, в случае чего ASP.NET
Core соответствующим образом пропустит и вернет только запрошенный набор
байтов. При этом прописывать в методах специальные обработчики для адаптации
или реализации этой функции не требуется, все будет сделано автоматически.
Запуск внешнего размещения и Application
Insights
Среды внешнего размещения теперь внедряют зависимости дополнительных
пакетов и выполняют код во время запуска приложения; при этом приложению не
нужно явно принимать зависимость или вызывать какие-либо методы. Эту
функцию можно использовать для того, чтобы выделить в определенных средах
какие-то уникальные для них компоненты без предварительной настройки самого
приложения.
В ASP.NET Core 2.0 эта функция используется для автоматического включения
диагностики Application Insights при отладке в Visual Studio и (после включения)
при запуске службы приложений Azure. В связи с этим шаблоны проектов больше
не добавляют пакеты и код Application Insights по умолчанию.
Сведения о состоянии плановой документации см. в статье о проблемах GitHub .
Автоматическое использование маркеров
защиты от подделки
ASP.NET Core всегда помогает в создании HTML-кода для содержимого, но в новой
версии сделан еще один шаг к предотвращению атак с подделкой межсайтовых
запросов (XSRF). Теперь ASP.NET Core будет выдавать маркеры защиты от подделки
по умолчанию и проверять их при отправке форм и выполнении страниц без
дополнительной конфигурации.
Дополнительные сведения см. на странице Предотвращение атак с
использованием подделки межсайтовых запросов (XSRF/CSRF) в ASP.NET Core.
Автоматическая предварительная
компиляция
При публикации по умолчанию включается предварительная компиляция
представления Razor, что сокращает размер выходных данных публикации и время
запуска приложения.
Дополнительные сведения см. в статье Компиляция и предварительная
компиляция представлений в ASP.NET Core.
Поддержка C# 7.1 в Razor
Для работы с новым компилятором Roslyn был обновлен и обработчик
представлений Razor. Добавлена поддержка таких функций C# 7.1 как выражения
по умолчанию, выводимые имена кортежей и сопоставление шаблонов с
универсальными шаблонами. Чтобы использовать C# 7.1 в проекте, добавьте в
файл проекта следующее свойство и перезагрузите решение.
XML
<LangVersion>latest</LangVersion>
Сведения о состоянии компонентов C# 7.1 см. в статье о репозитории Roslyn
GitHub .
Изменения в другой документации к
версии 2.0
Профили публикации Visual Studio для развертывания приложений ASP.NET
Core
Управление ключами
Настройка проверки подлинности Facebook
Настройка проверки подлинности Twitter
Настройка проверки подлинности Google
Настройка проверки подлинности учетной записи Майкрософт
Руководство по миграции
Инструкции по миграции приложений с ASP.NET Core 1.x в ASP.NET 2.0 см. в
следующих ресурсах.
Миграция с ASP.NET Core 1.x на ASP.NET Core 2.0
Миграция проверки подлинности и в ASP.NET Core 2.0
Дополнительные сведения
Полный список изменений см. в статье Заметки о выпуске ASP.NET Core 2.0 .
Чтобы отслеживать ход работы и планы команды разработчиков ASP.NET Core,
смотрите выпуски ASP.NET Community Standup .
Новые возможности ASP.NET Core 1.1
Статья • 28.01.2023 • Чтение занимает 2 мин
В состав ASP.NET Core 1.1 входят следующие новые функции и компоненты:
ПО промежуточного слоя для переопределения URL-адресов
ПО промежуточного слоя для кэширования ответов
Просмотр компонентов как вспомогательных функций тегов
ПО промежуточного слоя в качестве фильтров MVC
Поставщик TempData на основе файлов Cookie
Поставщик ведения журнала службы приложений Azure
Поставщик конфигурации Azure Key Vault
Репозитории ключей защиты данных для хранилищ Azure и Redis
Сервер WebListener для Windows
Поддержка WebSocket
Выбор между версиями ASP.NET Core 1.0 и 1.1
ASP.NET Core 1.1 имеет более широкий набор возможностей, чем ASP.NET Core 1.0.
Как правило, мы рекомендуем использовать последнюю версию.
Дополнительные сведения
Заметки о выпуске ASP.NET Core 1.1.0
Чтобы отслеживать ход работы и планы команды разработчиков ASP.NET
Core, смотрите выпуски ASP.NET Community Standup .
Выбор пользовательского вебинтерфейса ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 7 мин
ASP.NET Core — это полноценная платформа пользовательского интерфейса.
Выберите, какие функциональные возможности следует объединить, чтобы
соответствовать потребностям пользовательского веб-интерфейса приложения.
Преимущества и недостатки
пользовательских интерфейсов, которые
отрисовываются сервером и клиентом
Существует три общих подхода к созданию современного пользовательского вебинтерфейса в ASP.NET Core.
Приложения, которые преобразовывают для просмотра пользовательский
интерфейс с сервера.
Приложения, которые преобразовывают для просмотра пользовательский
интерфейс на клиенте в браузере.
Гибридные приложения, использующие преимущества методов отрисовки
серверных и клиентских пользовательских интерфейсов. Например,
большинство пользовательских веб-интерфейсов преобразовываются для
просмотра на сервере, а компоненты, отображаемые клиентом, добавляются
по мере необходимости.
Существует ряд преимуществ и недостатков, которые следует учитывать при
отрисовке пользовательского интерфейса на сервере или на клиенте.
Пользовательский интерфейс, отображаемый
сервером
Приложение пользовательского веб-интерфейса, которое преобразовывается для
просмотра на сервере, в ответ на запрос браузера динамически создает HTML и
CSS код страницы на сервере. Страница поступает на клиент уже готовая для
просмотра.
Преимущества:
Требования клиента минимальны, так как сервер создает логику и страницы:
Отлично подходит для низкоуровневых устройств и подключений с низкой
пропускной способностью.
Предоставляет широкий спектр версий браузеров на клиенте.
Быстрая загрузка начальной страницы.
Минимальное количество или отсутствие элементов JavaScript для
представления клиенту.
Гибкий доступ к защищенным ресурсам сервера:
Доступ к базе данных.
Доступ к секретам, таким как значения для вызовов API к службе
хранилища Azure.
Преимущества статического анализа сайта, такие как оптимизация для
поисковых систем.
Примеры распространенных сценариев приложения пользовательского вебинтерфейса, отображаемого сервером:
Динамические сайты, например те, которые предоставляют
персонализированные страницы, данные и формы.
Отображение данных только для чтения, например списков транзакций.
Отображение статических страниц блога.
Общедоступная система управления содержимым.
Недостатки:
Оплата за вычисления и использование памяти выставляется на сервере, а не
на каждом клиенте.
Для взаимодействия с пользователем требуется выполнить круговой путь к
серверу, чтобы создать обновления пользовательского интерфейса.
Пользовательский интерфейс, отображаемый
клиентом
Приложение, отображаемое клиентом, динамически преобразовывает для
просмотра пользовательский веб-интерфейс на клиенте и при необходимости
непосредственно обновляет модель DOM браузера.
Преимущества:
Обеспечивает практически мгновенные многочисленные интерактивные
возможности без необходимости выполнения кругового пути к серверу.
Обработка событий пользовательского интерфейса и логика выполняются
локально на устройстве пользователя с минимальной задержкой.
Поддержка добавочных обновлений с обеспечением сохранения частично
заполненных форм или документов, не требуя при этом от пользователя
нажимать кнопку для отправки формы.
Может быть настроен для работы в отключенном режиме. Обновления
клиентской модели с возможностью синхронизировать ее с сервером при
восстановлении подключения.
Снижение нагрузки и сокращение затрат на сервер. Рабочая нагрузка
переносится на клиент. Многие приложения, отображаемые клиентом, могут
размещаться так же, как и статические веб-сайты.
Использование преимуществ возможностей устройства пользователя.
Примеры пользовательского веб-интерфейса, отображаемого клиентом:
Интерактивная панель мониторинга.
Приложение с функцией перетаскивания.
Быстродействующее социальное приложение для совместной работы.
Недостатки:
Код логики необходимо скачать и выполнить на клиенте, что увеличивает
время начальной загрузки.
Требования клиента могут исключить пользователя, который использует
низкоуровневые устройства, более старые версии браузера или подключения
с низкой пропускной способностью.
Выбор решения пользовательского
интерфейса ASP.NET Core, отображаемого
сервером
В следующем разделе подробно описываются доступные модули
пользовательского веб-интерфейса ASP.NET Core, отображаемые сервером, и
приведены ссылки для начала работы. Razor Pages в ASP.NET Core и ASP.NET Core
MVC — это серверные платформы для создания веб-приложений с помощью .NET.
Razor Pages в ASP.NET Core
Razor Pages — это модель на основе страниц. Задачи, связанные с
пользовательским интерфейсом и бизнес-логикой, хранятся отдельно, но на одной
странице. Razor Pages — рекомендуемый способ создания приложений на основе
страниц или форм для разработчиков, которые еще не работали с ASP.NET Core.
Приступить к работе с Razor Pages намного проще, чем с ASP.NET Core MVC.
Преимущества Razor Pages в дополнение к преимуществам отрисовки сервера:
Быстрое создание и обновление пользовательского интерфейса. Код
страницы хранится на странице. Задачи, связанные с пользовательским
интерфейсом и бизнес-логикой, хранятся отдельно.
Возможность выполнить проверку и масштабирование больших приложений.
Более простой способ упорядочения страниц ASP.NET Core, чем при
использовании ASP.NET MVC:
Логику и модели представлений можно хранить вместе в их пространстве
имен и каталоге.
Группы связанных страниц могут храниться вместе в собственном
пространстве имен и каталоге.
Чтобы приступить к работе с первым приложением ASP.NET Core Razor Pages, см.
Учебник. Начало работы с Razor Pages в ASP.NET Core. Полный обзор ASP.NET Core
Razor Pages, его архитектуры и преимуществ см. в статье Введение в Razor Pages в
ASP.NET Core.
ASP.NET Core MVC
ASP.NET MVC отрисовывает пользовательский интерфейс на сервере и использует
шаблон архитектуры "Модель — представление — контроллер" (MVC). Шаблон
MVC разделяет приложение на три основных группы компонентов: модели,
представления и контроллеры. Запросы пользователей направляются в
контроллер. Контроллер отвечает за работу с моделью для выполнения действий
пользователей и получения результатов запросов. Контроллер выбирает
представление для отображения пользователю со всеми необходимыми данными
модели. Поддержка Razor Pages осуществляется на базе ASP.NET Core MVC.
Преимущества MVC в дополнение к преимуществам отрисовки сервера:
Основывается на масштабируемой и продуманной модели для создания
крупных веб-приложений.
Четкое разделение задач для максимальной гибкости.
Разделение обязанностей на основе шаблона "Модель — представление —
контроллер" гарантирует, что бизнес-модель можно легко развивать, не
затрагивая при этом реализацию возможностей более низкого уровня.
Сведения о начале работы с ASP.NET Core MVC см. в статье Начало работы с
ASP.NET Core MVC. Общие сведения об архитектуре и преимуществах ASP.NET Core
MVC см. в статье Общие сведения ASP.NET Core MVC.
Blazor Server
Платформа Blazor предназначена для создания интерактивного веб-интерфейса на
стороне клиента с использованием Blazor и предоставляет следующие
возможности:
создание многофункциональных интерактивных пользовательских
интерфейсов на C# вместо JavaScript ;
совместное использование серверной и клиентской логик приложений,
написанных с помощью .NET;
отображение пользовательского интерфейса в виде HTML-страницы с CSS для
широкой поддержки браузеров, в том числе для мобильных устройств.
интеграция с современными платформами размещения, такими как Docker.
создание гибридных классических и мобильных приложений с помощью .NET
и Blazor;
Использование .NET для разработки веб-приложений на стороне клиента
предоставляет следующие преимущества:
создавайте код на C#, а не на JavaScript.
возможность использовать существующую экосистему .NET с библиотеками
.NET;
сохранение единой логики приложений для сервера и клиента;
высокая производительность, надежность и безопасность платформы .NET;
Сохраняйте продуктивность в Windows, Linux или macOS с помощью среды
разработки, такой как Visual Studio
или Visual Studio Code .
создавайте приложения на основе распространенных языков, платформ и
инструментов, которые отличаются стабильностью, широким набором
функций и простотой в использовании.
Blazor Serverобеспечивает поддержку размещения пользовательского интерфейса,
отображаемого сервером, в приложении ASP.NET Core. Обновления
пользовательского интерфейса клиента обрабатываются через SignalR
подключение. Среда выполнения остается на сервере и обрабатывает выполнение
кода C# приложения.
Дополнительные сведения см. в статьях о моделях размещения ASP.NET Core Blazor
и ASP.NET Core Blazor. Модель размещения, отрисоченная Blazor клиентом, описана
Blazor WebAssembly в разделе далее в этой статье.
Выбор решения ASP.NET Core,
отображаемого клиентом
В следующем разделе кратко описываются доступные модули пользовательского
веб-интерфейса ASP.NET Core, отображаемые клиентом, и приведены ссылки для
начала работы.
Blazor WebAssembly
Blazor WebAssembly — это платформа одностраничных приложений (SPA) для
создания интерактивных клиентских веб-приложений с общими характеристиками
Blazor Server , описанными в разделе ранее в этой статье.
Выполнение кода .NET в веб-браузерах становится возможным благодаря
технологии WebAssembly
(сокращенно
). WebAssembly — это компактный
формат байт-кода, оптимизированный для быстрой загрузки и максимального
быстродействия. WebAssembly — это открытый веб-стандарт, который
поддерживается в веб-браузерах без подключаемых модулей. Blazor WebAssembly
работает во всех современных веб-браузерах, включая браузеры мобильных
устройств.
При сборке Blazor WebAssembly и запуске приложения:
Файлы кода C# и файлы Razor компилируются в сборки .NET.
Сборки и среда выполнения .NET загружаются в браузер.
Blazor WebAssembly осуществляет начальную загрузку среды выполнения .NET
и настраивает ее для загрузки сборок для приложения. Среда Blazor
WebAssembly выполнения использует взаимодействие JavaScript для
обработки операций с моделью DOM
и вызовов API браузера.
Дополнительные сведения см. в статьях о моделях размещения ASP.NET Core Blazor
и ASP.NET Core Blazor. Модель размещения, отрисоченная Blazor на сервере,
описана Blazor Server в разделе ранее в этой статье.
Одностраничное приложение (SPA) в ASP.NET Core с
платформами JavaScript, такими как Angular и React
Создайте клиентскую логику для приложений ASP.NET Core с помощью таких
популярных платформ JavaScript, как Angular
и React
. Приложение ASP.NET
Core предоставляет шаблоны проектов для Angular и React. Его также можно
использовать с другими платформами JavaScript.
Преимущества SPA в ASP.NET Core SPA с платформами JavaScript в дополнение к
приведенным выше преимуществам отрисовки клиента:
Среда выполнения JavaScript уже предоставлена вместе с браузером.
Большое сообщество и продуманная экосистема.
Создание клиентской логики для приложений ASP.NET Core с помощью таких
популярных платформ JS, как Angular и React.
Недостатки:
Требуются дополнительные языки программирования, платформы и средства.
Сложно обмениваться кодом, поэтому некоторая логика может
дублироваться.
Чтобы начать работу, см. следующие статьи.
Использование Angular с ASP.NET Core
Использование React с ASP.NET Core
Выбор гибридного решения: ASP.NET Core
MVC или Razor Pages в сочетании с Blazor
MVC, Razor Pages и Blazor являются частью платформы ASP.NET Core и разработаны
для совместного использования. Компоненты Razor можно интегрировать в
приложения Razor Pages и MVC в размещенном решении Blazor WebAssembly или
Blazor Server. Одновременно с отрисовкой страницы или представления можно
выполнять предварительную обработку компонентов.
Преимущества MVC или Razor Pages в сочетании с Blazor, которые дополняют
обычные преимуществам MVC и Razor Pages:
предварительная отрисовка выполняет компоненты Razor на сервере и
отрисовывает их в виде представления или страницы, что улучшает
восприятие скорости загрузки приложения.
Добавление интерактивности в существующие представления (страницы) с
помощью вспомогательной функции тега компонента.
Чтобы начать работу с ASP.NET Core MVC или Razor Pages в сочетании с Blazor,
воспользуйтесь статьей Компоненты Razor для предварительной визуализации и
интеграции ASP.NET Core.
Дальнейшие действия
Дополнительные сведения см. в разделе:
ASP.NET Core
Модели размещения ASP.NET Core
Компоненты Razor для предварительной визуализации и интеграции
ASP.NET Core
Сравнение служб gRPC с API-интерфейсами HTTP
Учебник. Создание веб-приложения
Razor Pages с помощью ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 2 мин
В этой серии руководств приводятся основные сведения о создании вебприложения Razor Pages.
Дополнительные сведения, предназначенные для разработчиков, которые
знакомы с контроллерами и представлениями, см. в статье Введение в Razor Pages
в ASP.NET Core.
Если вы новичок в разработке на ASP.NET Core и не знаете, какое решение для
пользовательского веб-интерфейса подойдет вам, см. статью Выбор
пользовательского интерфейса ASP.NET Core.
В серию входят следующие руководства:
1. Создание веб-приложения Razor Pages
2. Добавление модели в приложение Razor Pages
3. Формирование шаблона (создание) страницы Razor Pages
4. Работа с базой данных
5. Обновление Razor Pages
6. Добавление поиска
7. Добавление нового поля
8. Добавление проверки
В итоге вы получите приложение, которое позволяет отображать сведения о базе
данных фильмов и управлять ею.
Учебник. Начало работы с Razor Pages
в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 21 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом первом руководстве серии приводятся основные сведения о вебприложении Razor Pages в ASP.NET Core.
Дополнительные сведения, предназначенные для разработчиков, которые
знакомы с контроллерами и представлениями, см. в статье Введение в Razor Pages.
Общие сведения см. в разделе Entity Framework Core для начинающих .
Если вы новичок в разработке на ASP.NET Core и не знаете, какое решение для
пользовательского веб-интерфейса подойдет вам, см. статью Выбор
пользовательского интерфейса ASP.NET Core.
В конце этого руководства у Razor вас будет веб-приложение Pages, которое
управляет базой данных фильмов.
Предварительные требования
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создание веб-приложения Razor Pages
Visual Studio
Запустите Visual Studio и щелкните Создать проект
В диалоговом окне Создание проекта выберите ASP.NET Core Вебприложение>Далее.
В диалоговом окне Настроить новый проект введите RazorPagesMovie в
поле Имя проекта. Важно присвоить проекту имя RazorPagesMovie,
включая сопоставление регистра букв, чтобы пространство имени
соответствовало при копировании и вставке примера кода.
Выберите Далее.
В диалоговом окне Дополнительные сведения выполните следующие
действия.
Выберите .NET 7.0.
Убедитесь, что флажок Не использовать операторы верхнего уровня
снят.
Нажмите кнопку создания.
Создается следующий начальный проект:
Альтернативные подходы к созданию проекта см. в статье Создание проекта в
Visual Studio.
Запуск приложения
Visual Studio
Выберите RazorPagesMovie в Обозревателе решений и нажмите клавиши
CTRL + F5, чтобы выполнить запуск без отладчика.
Visual Studio отображает следующее диалоговое окно, если проект еще не
настроен для использования SSL:
Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.
Отобразится следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата
браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio:
Запускает приложение, которое запускает сервер Kestrel.
Запускает браузер по умолчанию по адресу https://localhost:<port> ,
который отображает пользовательский интерфейс приложений. <port> —
случайный порт, назначенный при создании приложения.
Анализ файлов проекта
В разделах ниже приведен обзор основных папок и файлов проекта, с которыми
вы будете работать в последующих учебниках.
Папка Pages
Содержит страницы Razor и вспомогательные файлы. Каждая страница Razor — это
пара файлов.
Файл .cshtml с разметкой HTML и кодом C# использует синтаксис Razor.
Файл .cshtml.cs с кодом C#, который обрабатывает события страницы.
Имена вспомогательных файлов начинаются с символа подчеркивания. Например,
файл _Layout.cshtml настраивает элементы пользовательского интерфейса, общие
для всех страниц. _Layout.cshtml настраивает меню навигации в верхней части
страницы и уведомление об авторских правах в нижней части страницы.
Подробные сведения см. в статье Макет в ASP.NET Core.
Папка wwwroot
Содержит статические ресурсы, такие как HTML-файлы, файлы JavaScript и CSSфайлы. Подробные сведения см. в статье Статические файлы в ASP.NET Core.
appsettings.json
Содержит данные конфигурации, например строки подключения. Дополнительные
сведения см. в разделе Конфигурация в ASP.NET Core.
Program.cs
Содержит следующий код:
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for
production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Следующие строки кода в этом файле создают WebApplicationBuilder с
предварительно настроенными значениями по умолчанию, добавляют Razor
поддержку Pages в контейнер внедрения зависимостей (DI) и создают приложение:
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
Страница со сведениями об исключении для разработчика включена по
умолчанию и содержит полезную информацию об исключениях. Рабочие
приложения не следует запускать в режиме разработки, поскольку на странице со
сведениями об исключении для разработчика может находиться
конфиденциальная информация.
В следующем коде для конечной точки устанавливаются исключения /Error и
включается протокол HSTS, если приложение не запущено в режиме разработки:
C#
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for
production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
Например, приведенный выше код выполняется, когда приложение находится в
рабочем или тестовом режиме. Дополнительные сведения см. в статье
Использование нескольких сред в ASP.NET Core.
Следующий код включает различное ПО промежуточного слоя:
app.UseHttpsRedirection(); : перенаправляет все запросы HTTP на HTTPS.
app.UseStaticFiles(); : обеспечивает обслуживание таких статических файлов,
как HTML, CSS, изображения и JavaScript. Подробные сведения см. в статье
Статические файлы в ASP.NET Core.
app.UseRouting(); : добавляет соответствие маршрута в конвейер ПО
промежуточного слоя. Дополнительные сведения см. в статье Маршрутизация
в ASP.NET Core.
app.MapRazorPages(); : настраивает маршрутизацию конечных точек для Razor
Pages.
app.UseAuthorization(); : разрешает пользователю доступ к защищенным
ресурсам. Это приложение не использует авторизацию, поэтому эту строку
можно удалить.
app.Run(); : запускает приложение.
Устранение неполадок с
завершенным примером
Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с
кодом готового проекта. Просмотрите или скачайте завершенный проект
(порядок загрузки).
Дальнейшие действия
Далее: Добавление модели
Часть 2. Добавление модели в
приложение Razor Pages в
ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 42 мин
В этом учебнике добавляются классы для управления фильмами в базе данных.
Классы моделей приложения используют Entity Framework Core (EF Core) для
работы с базой данных. EF Core — это объектно-реляционный сопоставителя
(O/RM), упрощающий доступ к данным. Сначала вы записываете классы модели и
EF Core создаете базу данных.
Классы модели называются классами POCO (из "Plain-Old CLR Objects"), так как они
не имеют зависимости EF Coreот . Эти классы определяют свойства данных,
которые хранятся в базе данных.
Добавление модели данных
Visual Studio
1. В Обозревателе решений щелкните правой кнопкой мыши проект
RazorPagesMovie и выберите Добавить>New Folder (Новая папка).
Назовите папку Models .
2. Щелкните правой кнопкой мыши папку Models . Выберите
Добавить>Класс. Присвойте классу имя Movie.
3. Добавьте в класс Movie следующие свойства:
C#
using System.ComponentModel.DataAnnotations;
namespace RazorPagesMovie.Models;
public class Movie
{
public int Id { get; set; }
public string? Title { get; set; }
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
public decimal Price { get; set; }
}
Класс Movie содержит:
Поле ID является обязательным для первичного ключа базы данных.
Атрибут [DataType], указывающий тип данных в свойстве ReleaseDate . С
этим атрибутом:
Пользователю не требуется вводить сведения о времени в поле даты.
Отображается только дата, а не время.
Знак вопроса после string указывает, что свойство допускает значение
NULL. Дополнительные сведения см. в статье Ссылочные типы,
допускающие значение NULL.
DataAnnotations рассматриваются в следующем руководстве.
Выполните сборку проекта, чтобы убедиться в отсутствии ошибок компиляции.
Создание модели фильма
В этом разделе создается модель фильма. То есть средство формирования
шаблонов создает страницы для операций создания, чтения, обновления и
удаления для модели фильма.
Visual Studio
1. Создайте папку Pages/Movies.
a. Щелкните правой кнопкой мыши папку Pages и выберите
Добавить>New Folder (Новая папка).
b. Назовите папку Movies.
2. Щелкните правой кнопкой мыши папку Pages/Movies и выберите
Добавить>New Scaffolded Item (Создать шаблонный элемент).
3. В диалоговом окне Добавление шаблона щелкните Razor Pages на
основе Entity Framework (CRUD)>Добавить.
4. Заполните поля в диалоговом окне Добавление Razor Pages на основе
Entity Framework (CRUD) :
a. В раскрывающемся списке Класс модели выберите Фильм
(RazorPagesMovie.Models) .
b. В строке Класс контекста данных щелкните знак плюса ( + ).
i. В диалоговом окне Добавление контекста данных будет
сгенерировано имя класса
RazorPagesMovie.Data.RazorPagesMovieContext .
c. Выберите Добавить.
Файл appsettings.json обновляется с указанием строки подключения,
используемой для подключения к локальной базе данных.
Созданные и обновленные файлы
В процессе формирования шаблонов создаются указанные ниже файлы.
Pages/Movies: Create, Delete, Details, Edit и Index.
Data/RazorPagesMovieContext.cs
В следующем учебнике приводится описание созданных файлов.
В процессе формирования шаблонов в файл Program.cs добавляется следующий
выделенный фрагмент кода.
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this
for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Изменения в файле Program.cs описываются далее в этом учебнике.
Создание начальной схемы базы данных с
помощью функции миграции EF
Функция миграции в Entity Framework Core предоставляет следующие
возможности:
Создание начальной схемы базы данных.
Поэтапное обновление схемы базы данных, чтобы она соответствовала
модели данных приложения. Существующие данные в базе данных
сохраняются.
Visual Studio
В этом разделе окно Консоль диспетчера пакетов (PMC) используется для
выполнения следующих действий:
Добавления первоначальной миграции.
Обновления базы данных с помощью первоначальной миграции.
1. В меню Инструменты выберите Диспетчер пакетов NuGet>Консоль
диспетчера пакетов.
2. В PMC введите следующие команды:
PowerShell
Add-Migration InitialCreate
Update-Database
Предыдущие команды:
Установите последнюю версию инструментов Entity Framework Core после
удаления любой предыдущей версии, если она существует.
Выполните команду , migrations чтобы создать код, который создает
исходную схему базы данных.
Появится следующее предупреждение, которое будет устранено на следующем
шаге:
"Для десятичного столбца Price в типе сущности Movie не указан тип. Это
приведет к тому, что значения будут усекаться без вмешательства
пользователя, если они не помещаются в значения точности и масштаба по
умолчанию. С помощью метода HasColumnType() явно укажите тип столбца
SQL Server, который может вместить все значения".
Команда migrations формирует код для создания схемы исходной базы данных.
Схема создается на основе модели, указанной в DbContext . Аргумент InitialCreate
используется для присвоения имен миграциям. Можно использовать любое имя,
однако по соглашению выбирается имя, которое описывает миграцию.
Команда update выполняет метод Up в миграциях, которые не были применены. В
этом случае update выполняет метод Up в файле Migrations/<timestamp>_InitialCreate.cs , который создает базу данных.
Проверка контекста, зарегистрированного с помощью
внедрения зависимостей
ASP.NET Core поддерживает внедрение зависимостей. Службы, такие как EF Core
контекст базы данных, регистрируются с помощью внедрения зависимостей во
время запуска приложения. Затем компоненты, которые используют эти службы
(например, Razor Pages), предоставляются через параметры конструктора. Код
конструктора, который получает экземпляр контекста базы данных, приведен далее
в этом руководстве.
Средство формирования шаблонов автоматически создает контекст базы данных и
регистрирует его с использованием контейнера внедрения зависимостей. Средство
формирования шаблонов добавляет следующий выделенный фрагмент кода в
файл Program.cs .
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this
for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Контекст данных RazorPagesMovieContext :
Получается из Microsoft.EntityFrameworkCore.DbContext.
Указывает сущности, которые включаются в модель данных.
EF Core Координирует функции, такие как создание, чтение, обновление и
удаление, для Movie модели.
C#
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
Microsoft.EntityFrameworkCore;
RazorPagesMovie.Models;
namespace RazorPagesMovie.Data
{
public class RazorPagesMovieContext : DbContext
{
public RazorPagesMovieContext
(DbContextOptions<RazorPagesMovieContext> options)
: base(options)
{
}
public DbSet<RazorPagesMovie.Models.Movie> Movie { get; set; } =
default!;
}
}
Представленный выше код создает свойство DbSet<Movie> для набора сущностей.
В терминологии Entity Framework набор сущностей обычно соответствует таблице
базы данных. Сущность соответствует строке в таблице.
Имя строки подключения передается в контекст путем вызова метода для объекта
DbContextOptions. При локальной разработке система конфигурации считывает
строку подключения из файла appsettings.json .
Тестирование приложения
1. Запустите приложение и добавьте /Movies к URL-адресу в браузере
( http://localhost:port/movies ).
Если вы получаете следующую ошибку:
Консоль
SqlException: Cannot open database "RazorPagesMovieContext-GUID"
requested by the login. The login failed.
Login failed for user 'User-name'.
Вы пропустили шаг миграции.
2. Протестируйте ссылку Create New (Создать).
7 Примечание
В поле Price нельзя вводить десятичные запятые. Чтобы обеспечить
поддержку проверки jQuery
для других языков, кроме английского,
используйте вместо десятичной точки запятую (,), а для отображения
данных в форматах для других языков, кроме английского, выполните
глобализацию приложения. Инструкции по глобализации см. на сайте
GitHub
.
3. Протестируйте ссылки Изменить, Сведения и Удалить.
В следующем учебнике рассматриваются файлы, созданные с помощью
формирования шаблонов.
Устранение неполадок с
завершенным примером
Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с
кодом готового проекта. Просмотрите или скачайте завершенный проект
(порядок загрузки).
Дополнительные ресурсы
Предыдущая статья. Начало работы
Следующая статья. Сформированные страницы Razor Pages
Часть 3. Razor Pages, созданные путем
формирования шаблонов, в
ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 22 мин
Автор: Рик Андерсон
(Rick Anderson)
Этот учебник описывает Razor Pages, созданные путем формирования шаблонов в
предыдущем учебнике.
Страницы Create, Delete, Details и Edit
Проверьте модель страницы Pages/Movies/Index.cshtml.cs :
C#
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies;
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
{
_context = context;
}
public IList<Movie> Movie { get;set; }
= default!;
public async Task OnGetAsync()
{
if (_context.Movie != null)
{
Movie = await _context.Movie.ToListAsync();
}
}
}
Razor Pages являются производными от класса PageModel. Как правило, класс,
производный от PageModel , называется PageNameModel . Например, страница Index
называется IndexModel .
Используя внедрение зависимостей, конструктор добавляет на страницу
RazorPagesMovieContext :
C#
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
{
_context = context;
}
Дополнительные сведения об асинхронном программировании с использованием
Entity Framework см. в разделе Асинхронный код.
При выполнении GET запроса на страницу OnGetAsync метод возвращает на
страницу список фильмов Razor . В Razor Page для инициализации состояния
страницы вызывается OnGetAsync или OnGet . В этом случае OnGetAsync возвращает
список фильмов для отображения.
Когда OnGet возвращает void или OnGetAsync возвращает Task , оператор return не
используется. Например, обратитесь к Privacy Page:
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesMovie.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}
}
Если возвращаемый тип — IActionResult или Task<IActionResult> , необходимо
предоставить оператор return. Например, метод Pages/Movies/Create.cshtml.cs
OnPostAsync :
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
Проверьте страницу Pages/Movies/Index.cshtml Razor:
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th></th>
=> model.Movie[0].Title)
=> model.Movie[0].ReleaseDate)
=> model.Movie[0].Genre)
=> model.Movie[0].Price)
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a>
|
<a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Razor может выполнять переход с HTML на C# или на разметку Razor. Если за
символом @ следует зарезервированное ключевое слово Razor, он переходит на
разметку Razor, а если нет, то на C#.
директиву @page
Директива Razor @page преобразует файл в действие MVC, а значит, он может
обрабатывать запросы. @page должна быть первой директивой Razor на странице.
@page и @model являются примерами перехода на разметку, относящуюся к Razor.
Дополнительные сведения см. в статье Синтаксис Razor.
директиву @model
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
Директива @model определяет тип модели, передаваемой на страницу Razor. В
приведенном выше примере строка @model делает класс, производный от
PageModel , доступным для Razor Page. Модель используется на странице во
вспомогательных методах HTML @Html.DisplayNameFor и @Html.DisplayFor .
Проверьте лямбда-выражение, которое используется в следующем
вспомогательном методе HTML:
CSHTML
@Html.DisplayNameFor(model => model.Movie[0].Title)
Вспомогательный метод HTML DisplayNameFor проверяет свойство Title ,
указанное в лямбда-выражении, и определяет отображаемое имя. Лямбдавыражение проверяется, а не вычисляется. Это означает, что в случае, если model ,
model.Movie или model.Movie[0] имеют значение null или пусты, права доступа не
нарушаются. При вычислении лямбда-выражения, например с помощью
@Html.DisplayFor(modelItem => item.Title) , вычисляются значения для свойств
модели.
Страница макета
Выберите ссылки в меню (RazorPagesMovie, Home и Privacy). Меню на каждой
странице имеют одинаковый макет. Макет меню реализован в файле
Pages/Shared/_Layout.cshtml .
Откройте файл Pages/Shared/_Layout.cshtml и проверьте его.
Шаблоны макета позволяют сделать следующее для макета контейнера HTML:
указать его в одном расположении;
применить его на нескольких страницах сайта.
Найдите строку @RenderBody() . RenderBody — это заполнитель для отображения всех
представлений определенных страниц, упакованных в страницу макета. Например,
щелкните ссылку Privacy, и представление Pages/Privacy.cshtml отобразится в
методе RenderBody .
ViewData и макет
Рассмотрим следующую разметку из файла Pages/Movies/Index.cshtml .
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
Выделенная выше разметка представляет собой пример перехода Razor на C#.
Символы { и } ограничивают блок кода C#.
Базовый класс PageModel содержит свойство словаря ViewData . Оно позволяет
передать данные в представление. Объекты добавляются в словарь ViewData с
помощью шаблона ключ — значение. В приведенном выше примере в словарь
ViewData добавляется свойство Title .
Свойство Title используется в файле Pages/Shared/_Layout.cshtml . Ниже показаны
первые несколько строк файла _Layout.cshtml .
CSHTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - RazorPagesMovie</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"
/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"
/>
<link rel="stylesheet" href="~/RazorPagesMovie.styles.css" asp-appendversion="true" />
Строка @*Markup removed for brevity.*@ представляет собой комментарий Razor. В
отличие от комментариев HTML <!-- --> комментарии Razor не отправляются
клиенту. Дополнительные сведения см. в веб-документации MDN. Начало работы с
HTML .
Обновление макета
1. Измените элемент <title> в файле Pages/Shared/_Layout.cshtml так, чтобы
вместо Movie отображалось RazorPagesMovie.
CSHTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initialscale=1.0" />
<title>@ViewData["Title"] - Movie</title>
2. Найдите следующий элемент привязки в файле Pages/Shared/_Layout.cshtml .
CSHTML
<a class="navbar-brand" asp-area="" asppage="/Index">RazorPagesMovie</a>
3. Измените указанный выше элемент на следующую разметку.
CSHTML
<a class="navbar-brand" asp-page="/Movies/Index">RpMovie</a>
Указанный выше элемент привязки является вспомогательной функцией тега.
В данном случае он является вспомогательной функцией тега привязки.
Атрибут вспомогательной функции тега asp-page="/Movies/Index" и его
значение создают ссылку на страницу Razor /Movies/Index . Атрибут asp-area
имеет пустое значение, поэтому эта область не используется в ссылке.
Дополнительные сведения см. в статье Области.
4. Сохраните изменения и протестируйте приложение, выбрав ссылку RpMovie.
Если у вас возникли проблемы, ознакомьтесь с файлом _Layout.cshtml
в
GitHub.
5. Проверьте ссылки Home, RpMovie, Create, Edit и Delete. Каждая страница
задает заголовок, который отображается на вкладке браузера. При
добавлении страницы в избранное заголовок используется в закладках.
7 Примечание
В поле Price нельзя вводить десятичные запятые. Чтобы обеспечить
поддержку проверки jQuery
для других языков, кроме английского,
используйте вместо десятичной точки запятую (","), а для отображения данных
в форматах для других языков, кроме английского, выполните действия,
необходимые для глобализации приложения. Инструкции по добавлению
десятичной запятой см. в вопросе № 4076 на сайте GitHub .
Свойство Layout задается в файле Pages/_ViewStart.cshtml :
CSHTML
@{
Layout = "_Layout";
}
Представленный выше код задает файл разметки Pages/Shared/_Layout.cshtml для
всех файлов Razor в папке Pages. Дополнительные сведения см. в статье о макете.
Страничная модель Create
Изучите страничную модель Pages/Movies/Create.cshtml.cs :
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext
_context;
public CreateModel(RazorPagesMovie.Data.RazorPagesMovieContext
context)
{
_context = context;
}
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
public Movie Movie { get; set; } = default!;
// To protect from overposting attacks, see
https://aka.ms/RazorPagesCRUD
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid || _context.Movie == null || Movie ==
null)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Метод OnGet инициализирует все состояния, необходимые для страницы. Страница
Create не содержит никаких состояний для инициализации, поэтому возвращается
Page . Далее в этом руководстве показан пример инициализации состояния OnGet .
Метод Page создает объект PageResult , который формирует страницу
Create.cshtml .
Для указания согласия на привязку модели в свойстве Movie используется атрибут
[BindProperty]. Когда форма Create публикует свои значения, среда выполнения
ASP.NET Core связывает переданные значения с моделью Movie .
Метод OnPostAsync выполняется, когда страница публикует данные формы:
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
Если в модели есть ошибки, форма отображается снова вместе со всеми
опубликованными данными этой формы. Большинство ошибок в модели может
быть перехвачено на стороне клиента до публикации формы. Пример ошибки в
модели — это публикация значения для поля даты, которое нельзя конвертировать
в дату. Проверка на стороне клиента и проверка модели обсуждаются подробнее
далее в этом учебнике.
Если нет ошибок модели:
Данные сохранены.
Браузер перенаправляется на страницу Index.
Страница Razor создания
Проверьте файл страницы Pages/Movies/Create.cshtml Razor:
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.CreateModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label">
</label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary"
/>
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Visual Studio
Visual Studio выделяет следующие теги полужирным шрифтом, который
используется для вспомогательных функций тегов.
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
Элемент <form method="post"> представляет собой вспомогательную функцию тега
Form. Вспомогательная функция тега Form автоматически включает маркер защиты
от подделки.
Ядро формирования шаблонов создает разметку Razor для каждого поля в модели
(кроме ID) следующего вида:
CSHTML
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
Вспомогательные функции тегов Validation ( <div asp-validation-summary и <span
asp-validation-for ) отображают ошибки проверки. Более подробно проверка
рассматривается далее в этой серии статей.
Вспомогательная функция тега Label ( <label asp-for="Movie.Title" class="controllabel"></label> ) создает подпись к метке и атрибут [for] для свойства Title .
Вспомогательная функция тега Input ( <input asp-for="Movie.Title" class="formcontrol"> ) использует атрибуты DataAnnotations и создает HTML-атрибуты,
необходимые для проверки jQuery на стороне клиента.
Дополнительные сведения о вспомогательных функциях тегов, таких как <form
method="post"> , см. в статье Вспомогательные функции тегов в ASP.NET Core.
Дополнительные ресурсы
Предыдущая статья. Добавление модели
Следующая статья. Работа с базой данных
Часть 4 серии руководств по Razor
Pages
Статья • 28.01.2023 • Чтение занимает 18 мин
Джо Одетт
Объект RazorPagesMovieContext обрабатывает задачу подключения к базе данных и
сопоставления объектов Movie с записями базы данных. Контекст базы данных
регистрируется с помощью контейнера внедрения зависимостей в файле :
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
Система конфигурации ASP.NET Core считывает ключ . Для разработки на
локальном уровне конфигурация получает строку подключения из файла
appsettings.json .
Visual Studio
Созданная строка подключения выглядит аналогично следующему коду JSON.
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"RazorPagesMovieContext": "Server=
(localdb)\\mssqllocaldb;Database=RazorPagesMovieContextbc;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Если приложение развертывается на тестовом или рабочем сервере, можно задать
строку подключения к тестовому или рабочему серверу базы данных с помощью
переменной среды. Дополнительные сведения см. в разделе Конфигурация.
Visual Studio
SQL Server Express LocalDB
LocalDB — это упрощенная версия ядра СУБД SQL Server Express,
предназначенная для разработки программ. LocalDB запускается по запросу в
пользовательском режиме, поэтому настройки не слишком сложны. По
умолчанию база данных LocalDB создает файлы *.mdf в каталоге C:\Users\
<user>\ .
1. В меню Вид откройте обозреватель объектов SQL Server (SSOX).
2. Щелкните правой кнопкой мыши таблицу Movie и выберите пункт Movie :
Обратите внимание на значок с изображением ключа рядом с ID . По
умолчанию EF создает свойство с именем ID для первичного ключа.
3. Щелкните правой кнопкой мыши таблицу Movie и выберите пункт Movie :
Заполнение базы данных
Создайте класс SeedData в папке SeedData со следующим кодом:
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
namespace RazorPagesMovie.Models;
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RazorPagesMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<RazorPagesMovieContext>>()))
{
if (context == null || context.Movie == null)
{
throw new ArgumentNullException("Null
RazorPagesMovieContext");
}
// Look for any movies.
if (context.Movie.Any())
{
return;
// DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
Если в базе данных есть фильмы, возвращается инициализатор заполнения и
фильмы не добавляются.
C#
if (context.Movie.Any())
{
return;
}
Добавление инициализатора заполнения
Обновите файл Program.cs , используя следующий выделенный код:
Visual Studio
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
using RazorPagesMovie.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RazorPag
esMovieContext") ?? throw new InvalidOperationException("Connection
string 'RazorPagesMovieContext' not found.")));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
В приведенном выше коде файл Program.cs изменен, чтобы сделать следующее:
Получение экземпляра контекста базы данных из контейнера внедрения
зависимостей (DI).
Вызовите метод seedData.Initialize , передав ему экземпляр контекста базы
данных.
Высвобождение контекста после завершения работы метода заполнения.
Оператор using гарантирует удаление контекста.
Если Update-Database не выполнялось, возникает следующее исключение:
SqlException: Cannot open database "RazorPagesMovieContext-" requested by the
login. The login failed. Login failed for user 'user name'.
Тестирование приложения
Удалите все записи в базе данных для запуска метода seed. Остановите и запустите
приложение, чтобы начать заполнение базы данных. Если база данных не
заполнена, установите точку останова в if (context.Movie.Any()) и пошагово
выполните код.
В приложении отображаются заполненные данные.
Дополнительные ресурсы
Предыдущая статья. Сформированные страницы
Далее: Обновление страниц
Pages
Часть 5. Изменение созданных
страниц в приложении ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 13 мин
Приложение для работы с фильмами подготовлено, но представление данных
далеко от идеала. Вместо ReleaseDate должно стоять Дата выпуска, то есть два
слова.
Обновление модели
Обновите файл Models/Movie.cs , используя следующий выделенный код:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
public class Movie
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; } = string.Empty;
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
}
В предыдущем коде:
Заметка к данным [Column(TypeName = "decimal(18, 2)")] позволяет Entity
Framework Core корректно сопоставить Price с валютой в базе данных.
Дополнительные сведения см. в разделе Типы данных.
Атрибут [Display] указывает на отображаемое имя поля. В приведенном выше
коде Release Date вместо ReleaseDate .
Атрибут [DataType] указывает тип данных ( Date ). Сведения о времени,
хранящиеся в поле, не отображаются.
Пространство имен DataAnnotations будет рассмотрено в следующем руководстве.
Перейдите к Pages/Movies и наведите указатель мыши на ссылку Edit (Изменение),
чтобы просмотреть целевой URL-адрес.
Ссылки Edit, Details и Delete создаются вспомогательной функцией тегов привязки
в файле Pages/Movies/Index.cshtml .
CSHTML
@foreach (var item in Model.Movie) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a>
|
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Вспомогательные функции тегов позволяют серверному коду участвовать в
создании и отображении HTML-элементов в файлах Razor.
В приведенном выше коде Вспомогательная функция привязки тегов динамически
создает значение атрибута HTML href на основе Razor Page (маршрут является
относительным), атрибут asp-page и идентификатор маршрута ( asp-route-id ).
Дополнительные сведения см. в разделе Формирование URL-адресов для страниц.
Для проверки созданной разметки используйте в браузере параметр Просмотреть
исходный код. Ниже показана часть созданного кода HTML:
HTML
<td>
<a href="/Movies/Edit?id=1">Edit</a> |
<a href="/Movies/Details?id=1">Details</a> |
<a href="/Movies/Delete?id=1">Delete</a>
</td>
Динамически создаваемые ссылки передают идентификатор фильма со строкой
запроса . Например, ?id=1 в https://localhost:5001/Movies/Details?id=1 .
Добавление шаблона маршрута
Обновите страницы Razor Pages Edit, Details и Delete так, чтобы использовался
шаблон маршрута {id:int} . Измените директиву страницы для каждой из этих
страниц c @page на @page "{id:int}" . Запустите приложение и просмотрите
исходный код.
Созданный код HTML добавляет идентификатор в путь URL-адреса:
HTML
<td>
<a href="/Movies/Edit/1">Edit</a> |
<a href="/Movies/Details/1">Details</a> |
<a href="/Movies/Delete/1">Delete</a>
</td>
Запрос на страницу с шаблоном {id:int} маршрута, который не содержит целое
число, возвращает ошибку HTTP 404 (не найдено). Например,
https://localhost:5001/Movies/Details возвращает ошибку 404. Чтобы сделать
идентификатор необязательным, добавьте ? к ограничению маршрута:
CSHTML
@page "{id:int?}"
Чтобы проверить поведение @page "{id:int?}" :
1. Задайте директиву страницы в Pages/Movies/Details.cshtml как @page "
{id:int?}" .
2. Установите точку останова в public async Task<IActionResult> OnGetAsync(int?
id) , в Pages/Movies/Details.cshtml.cs .
3. Перейдите к https://localhost:5001/Movies/Details/ .
Из-за директивы @page "{id:int}" точка останова не достигается. Механизм
маршрутизации возвращает ошибку HTTP 404. При использовании @page "
{id:int?}" метод OnGetAsync возвращает NotFound (HTTP 404):
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Movie = await _context.Movie.FirstOrDefaultAsync(m => m.ID == id);
if (Movie == null)
{
return NotFound();
}
return Page();
}
Проверка обработки исключений нежесткой
блокировки
Проверьте метод OnPostAsync в файле Pages/Movies/Edit.cshtml.cs .
C#
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool MovieExists(int id)
{
return _context.Movie.Any(e => e.Id == id);
}
Предыдущий код обнаруживает исключения параллелизма, когда один клиент
удаляет фильм, а другой вносит изменения в фильм.
Чтобы протестировать блок catch , выполните указанные ниже действия.
1. Задайте точку останова в catch (DbUpdateConcurrencyException) .
2. Выберите команду Изменить у фильма, внесите изменения, но не вводите
Сохранить.
3. В другом окне браузера щелкните ссылку Delete для этого же фильма, а затем
удалите его.
4. В первом окне браузера опубликуйте изменения для фильма.
Коду в рабочей среде может потребоваться обнаружение конфликтов
параллелизма. Дополнительные сведения см. в статье Обработка конфликтов
параллелизма.
Проверка публикации и привязки
Просмотрите файл Pages/Movies/Edit.cshtml.cs .
C#
public class EditModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext _context;
public EditModel(RazorPagesMovie.Data.RazorPagesMovieContext context)
{
_context = context;
}
[BindProperty]
public Movie Movie { get; set; } = default!;
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null || _context.Movie == null)
{
return NotFound();
}
var movie =
await _context.Movie.FirstOrDefaultAsync(m => m.Id ==
id);
if (movie == null)
{
return NotFound();
}
Movie = movie;
return Page();
}
// To protect from overposting attacks, enable the specific properties
you want to bind to.
// For more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(Movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool MovieExists(int id)
{
return _context.Movie.Any(e => e.Id == id);
}
При выполнении HTTP-запроса GET к странице Movies/Edit, например
https://localhost:5001/Movies/Edit/3 , происходит следующее:
Метод OnGetAsync извлекает запись фильма из базы данных и возвращает
метод Page .
Метод Page отображает страницу Pages/Movies/Edit.cshtml Razor. Файл
Pages/Movies/Edit.cshtml содержит директиву модели @model
RazorPagesMovie.Pages.Movies.EditModel , которая делает модель фильма
доступной на странице.
Отображается форма Edit со значениями из записи фильма.
При публикации страницы Movies/Edit происходит следующее:
Значения формы на странице привязываются к свойству Movie . Атрибут
[BindProperty] обеспечивает привязку модели.
C#
[BindProperty]
public Movie Movie { get; set; }
При наличии ошибок в состоянии модели, например ReleaseDate невозможно
преобразовать в дату, форма отображается повторно с предоставленными
значениями.
Если ошибки модели отсутствуют, данные фильма сохраняются.
Методы HTTP GET на страницах Razor Index, Create и Delete работают аналогично.
Метод HTTP POST OnPostAsync на странице Razor Create работает аналогично
методу OnPostAsync на странице Razor Edit.
Дополнительные ресурсы
Предыдущая статья. Работа с базой данных
Следующая статья. Добавление поиска
Часть 6. Добавление поиска в Razor
Pages в ASP.NET Core
Статья • 10.01.2023 • Чтение занимает 8 мин
Автор: Рик Андерсон
(Rick Anderson)
В следующих разделах добавляется поиск фильмов по жанру или имени.
Добавьте выделенный ниже код в Pages/Movies/Index.cshtml.cs :
C#
using
using
using
using
using
using
using
using
Microsoft.AspNetCore.Mvc;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.AspNetCore.Mvc.Rendering;
Microsoft.EntityFrameworkCore;
RazorPagesMovie.Models;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
namespace RazorPagesMovie.Pages.Movies
{
public class IndexModel : PageModel
{
private readonly RazorPagesMovie.Data.RazorPagesMovieContext
_context;
public IndexModel(RazorPagesMovie.Data.RazorPagesMovieContext
context)
{
_context = context;
}
public IList<Movie> Movie { get;set; } = default!;
[BindProperty(SupportsGet = true)]
public string ? SearchString { get; set; }
public SelectList ? Genres { get; set; }
[BindProperty(SupportsGet = true)]
public string ? MovieGenre { get; set; }
В предыдущем коде:
SearchString : содержит текст, который пользователи вводят в поле поиска.
SearchString также имеет атрибут [BindProperty]. [BindProperty] связывает
значения из формы и строки запроса с тем же именем, что и у свойства.
[BindProperty(SupportsGet = true)] требуется для привязки в запросах
HTTP GET.
Genres : содержит список жанров. Genres дает пользователю возможность
выбрать жанр в списке. Для SelectList требуется using
Microsoft.AspNetCore.Mvc.Rendering; .
MovieGenre : содержит конкретный жанр, выбранный пользователем.
Например, "Боевик".
Genres и MovieGenre рассматриваются позднее в этом учебнике.
2 Предупреждение
В целях безопасности вам следует задать привязку данных запроса GET к
свойствам страничной модели. Проверьте введенные данные пользователя,
прежде чем сопоставлять их со свойствами. Привязка GET полезна при
обращении к сценариям, использующим строку запроса или значения
маршрутов.
Чтобы привязать свойство к запросам GET , задайте для свойства SupportsGet
атрибута [BindProperty] значение true :
C#
[BindProperty(SupportsGet = true)]
Дополнительные сведения: ASP.NET Core Community Standup: Bind on GET
discussion (YouTube) .
Обновите метод OnGetAsync страницы Index, добавив следующий код:
C#
public async Task OnGetAsync()
{
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
Movie = await movies.ToListAsync();
}
В первой строке метода OnGetAsync создается запрос LINQ для выбора фильмов:
C#
// using System.Linq;
var movies = from m in _context.Movie
select m;
Этот запрос только определяется в этой точке и не выполняется для базы данных.
Если свойство SearchString не равно NULL и не пусто, запрос фильмов изменяется
для фильтрации по строке поиска:
C#
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
Код s => s.Title.Contains() представляет собой лямбда-выражение. Лямбдавыражения используются в запросах LINQ на основе методов в качестве
аргументов стандартных методов операторов запроса, таких как метод Where или
Contains . Запросы LINQ не выполняются, если они определяются или изменяются
путем вызова метода, например Where , Contains или OrderBy . Вместо этого
выполнение запроса откладывается. Вычисление выражения откладывается до тех
пор, пока не будет выполнена итерация его реализованного значения или не будет
вызван метод ToListAsync . Дополнительные сведения см. в разделе Выполнение
запроса.
7 Примечание
Метод Contains выполняется в базе данных, а не в коде C#. Регистр символов
запроса учитывается в зависимости от параметров базы данных и сортировки.
В SQL Server метод Contains сопоставляется с SQL LIKE, в котором регистр
символов не учитывается. SQLite с параметрами сортировки по умолчанию —
это смесь параметров с учетом регистра и параметров, которые НЕ учитывают
регистр, в зависимости от запроса. Сведения о том, как выполнять запросы
SQLite, которые не учитывают регистр, см. в следующих статьях:
Эта проблема в GitHub
Эта проблема в GitHub
Параметры сортировки и учет регистра
Перейдите на страницу Movies и добавьте строку запроса, например ?
searchString=Ghost , к URL-адресу. Например, https://localhost:5001/Movies?
searchString=Ghost . Отображаются отфильтрованные фильмы.
Если на страницу Index добавлен следующий шаблон маршрута, строку поиска
можно передать в виде сегмента URL-адреса. Например,
https://localhost:5001/Movies/Ghost .
CSHTML
@page "{searchString?}"
Предыдущее ограничение маршрута разрешало поиск названия в виде данных
маршрута (сегмент URL-адреса) вместо значения строки запроса. Символ ? в "
{searchString?}" означает, что этот параметр является необязательным.
Среда выполнения ASP.NET Core использует привязку модели, чтобы присвоить
значение свойства SearchString по строке запроса ( ?searchString=Ghost ) или
данным маршрута ( https://localhost:5001/Movies/Ghost ). Привязка модели не
учитывает регистр символов.
Однако пользователи не могут изменять URL-адрес для поиска фильма. На этом
шаге добавляется пользовательский интерфейс для поиска фильмов. Если было
добавлено ограничение маршрута "{searchString?}" , удалите его.
Откройте файл Pages/Movies/Index.cshtml и добавьте разметку, которая выделена в
следующем коде:
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
@*Markup removed for brevity.*@
Тег HTML <form> использует следующие вспомогательные функции тегов:
вспомогательная функция тега форм. При отправке формы строка фильтра
отправляется на страницу Pages/Movies/Index в строке запроса.
Вспомогательная функция тега Input
Сохраните изменения и проверьте работу фильтра.
Поиск по жанру
Обновите метод OnGetAsync страницы Index, добавив следующий код:
C#
public async Task OnGetAsync()
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(SearchString))
{
movies = movies.Where(s => s.Title.Contains(SearchString));
}
if (!string.IsNullOrEmpty(MovieGenre))
{
movies = movies.Where(x => x.Genre == MovieGenre);
}
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Movie = await movies.ToListAsync();
}
Следующий код определяет запрос LINQ, который извлекает все жанры из базы
данных.
C#
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
Список жанров SelectList создается путем проецирования отдельных жанров.
C#
Genres = new SelectList(await genreQuery.Distinct().ToListAsync());
Добавление поиска по жанру на страницу Razor
Обновите Index.cshtml элемент <form> , как показано в следующей разметке:
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и
по обоим этим параметрам.
Предыдущая статья. Обновление страниц
Следующая статья. Добавление нового поля
Часть 7. Добавление нового поля на
страницу Razor в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 20 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом разделе Entity Framework Code First Migrations используется для выполнения
следующих задач:
Добавление нового поля в модель.
Перенос изменений в схеме нового поля в базу данных.
Если вы используете EF Code First для автоматического создания и отслеживания
базы данных, Code First:
добавляет в базу данных таблицу __EFMigrationsHistory, которая позволяет
отслеживать синхронизацию схемы базы данных с классами модели, на
основе которой она была создана.
Если классы модели не синхронизированы с базой данных, выдает
исключение.
Автоматическая проверка синхронизации схемы и модели упрощает поиск
несогласованных проблем в коде базы данных.
Добавление свойства Rating в модель Movie
1. Откройте файл Models/Movie.cs и добавьте свойство Rating :
C#
public class Movie
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; } = string.Empty;
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
public string Rating { get; set; } = string.Empty;
}
2. Отредактируйте Pages/Movies/Index.cshtml и добавьте поле Rating :
CSHTML
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<form>
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
model.Movie[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movie)
{
<tr>
=> model.Movie[0].Title)
=>
=> model.Movie[0].Genre)
=> model.Movie[0].Price)
=> model.Movie[0].Rating)
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
<a asp-page="./Edit" asp-routeid="@item.Id">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.Id">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
3. Обновите следующие страницы, добавив поле Rating :
Pages/Movies/Create.cshtml
.
Pages/Movies/Delete.cshtml
.
Pages/Movies/Details.cshtml
.
Pages/Movies/Edit.cshtml
.
Для работы приложения необходимо обновить базу данных, включив в нее новое
поле. При запуске приложения без обновления базы данных возникает
SqlException :
SqlException: Invalid column name 'Rating'.
Исключение SqlException связано с тем, что обновленный класс модели Movie
отличается от схемы таблицы Movie в базе данных. В таблице базы данных нет
столбца Rating .
Устранить эту ошибку можно несколькими способами:
1. Можно с помощью Entity Framework автоматически удалить и повторно
создать базу данных на основе новой схемы класса модели. Этот подход
удобен на ранних стадиях цикла разработки. В этом случае развитие модели и
схемы базы данных осуществляется одновременно. Недостатком такого
подхода является потеря существующих данных в базе. В рабочей базе
данных применять этот подход не следует! При разработке приложения часто
выполняется удаление базы данных при изменении схемы, для чего
используется инициализатор для автоматического заполнения базы
тестовыми данными.
2. Можно явно изменить схему существующей базы данных в соответствии с
новыми классами модели. Преимущество подхода в том, что он сохраняет
данные. Внесите это изменение вручную либо путем создания скрипта
изменения для базы данных.
3. Можно обновить схему базы данных с помощью Code First Migrations.
В этом руководстве используется Code First Migrations.
Обновите класс SeedData так, чтобы он предоставлял значение нового столбца.
Ниже приведен пример изменения, которое необходимо реализовать для каждого
блока new Movie .
C#
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M,
Rating = "R"
},
См. готовый файл SeedData.cs .
Постройте решение.
Visual Studio
Добавление миграции для поля Rating
1. В меню Сервис последовательно выберите пункты Диспетчер пакетов
NuGet > Консоль диспетчера пакетов.
2. В PMC введите следующие команды:
PowerShell
Add-Migration Rating
Update-Database
Команда Add-Migration задает следующие инструкции для платформы:
Сравните модель Movie со схемой базы данных Movie .
Создайте код для переноса схемы базы данных в новую модель.
В качестве имени файла переноса используется произвольное имя "Rating".
Рекомендуется присваивать этому файлу понятное имя.
Команда Update-Database указывает платформе, что к базе данных нужно
применить изменения схемы, а также сохранить существующие данные.
Если удалить все записи из базы данных, при инициализации она будет
заполнена начальными значениями и в нее будет включено поле Rating . Это
можно сделать с помощью ссылок удаления в браузере или из обозревателя
объектов SQL Server (SSOX).
Другой вариант — удалить базу данных и использовать миграции для
повторного создания базы данных. Удаление базы данных в SSOX:
1. Выберите базу данных в SSOX.
2. Щелкните базу данных правой кнопкой мыши и выберите Удалить.
3. Выберите Закрыть существующие соединения.
4. Нажмите кнопку ОК.
5. Обновите базу данных в PMC.
PowerShell
Update-Database
Запустите приложение и проверьте возможность создания, редактирования и
отображения фильмов с использованием поля Rating . Если база данных не
заполнена начальными значениями, задайте точку останова в методе
SeedData.Initialize .
Дополнительные ресурсы
Предыдущая статья. Добавление поиска
Следующая статья. Добавление проверки
Часть 8 серии руководств по Razor
Pages
Статья • 28.01.2023 • Чтение занимает 26 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом разделе к модели Movie добавляется логика проверки. Правила проверки
применяются каждый раз, когда пользователь создает или редактирует фильм.
Проверка
Ключевой принцип разработки программного обеспечения называется DRY
(от
английского "Don't Repeat Yourself" — не повторяйся). При разработке Razor Pages
рекомендуется задавать любые функциональные возможности лишь один раз и
затем при необходимости отражать их в рамках всего приложения. Принцип "Не
повторяйся" может помочь:
сократить объем кода в приложении;
снизить вероятность возникновения ошибки в коде и упростить его
тестирование и поддержку.
Ярким примером применения принципа "Не повторяйся" является поддержка
проверки, реализуемая в Razor Pages и на платформе Entity Framework:
Правила проверки декларативно определяются в одном месте, в классе
модели.
Правила применяются по всему приложению.
Добавление правил проверки к модели
фильма
Пространство имен System.ComponentModel.DataAnnotations предоставляет:
Набор встроенных атрибутов проверки, которые декларативно применяются
к классу или свойству.
Атрибуты форматирования (такие как [DataType] ), которые обеспечивают
форматирование и не предназначены для проверки.
Обновите класс Movie , чтобы использовать преимущества встроенных атрибутов
проверки [Required] , [StringLength] , [RegularExpression] и [Range] .
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
public class Movie
{
public int Id { get; set; }
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
// [Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; } = string.Empty;
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; } = string.Empty;
}
Атрибуты проверки определяют поведение для свойств модели, к которым они
применяются:
Атрибуты [Required] и [MinimumLength] означают, что свойство должно иметь
значение. Пользователь может свободно вводить пробелы для выполнения
этой проверки.
Атрибут [RegularExpression] ограничивает набор допустимых для ввода
символов. В приведенном выше коде в Genre :
должны использоваться только буквы;
первая буква должна быть прописной; Пробелы разрешены, а цифры и
специальные символы — нет.
RegularExpression Rating :
первый символ должен быть прописной буквой;
допускаются специальные символы и цифры, а также последующие
пробелы. значение "PG-13" допустимо для рейтинга, но недопустимо для
Genre ;
атрибут [Range] ограничивает значения указанным диапазоном.
Атрибут [StringLength] может задавать максимальную и, при необходимости,
минимальную длину строкового свойства.
Типы значений (например, decimal , int , float , DateTime ) по своей природе
являются обязательными и не требуют атрибута [Required] .
Указанные выше правила проверки используются для демонстрации, они не
являются оптимальными для рабочей системы. Например, предыдущее
исключение не позволяет ввести модель Movie с двумя символами и не допускает
специальные знаки в Genre .
Наличие правил проверки, которые автоматически применяются ASP.NET Core,
помогает:
сделать приложение более надежным;
сократить возможность сохранения недопустимых данных в базе данных.
Пользовательский интерфейс проверки ошибок в
Razor Pages
Запустите приложение и перейдите в раздел "Pages/Movies" (Страницы/фильмы).
Щелкните ссылку Create New (Создать). Введите в форму какие-либо недопустимые
значения. Если функция проверки jQuery на стороне клиента обнаруживает
ошибку, сведения о ней отображаются в соответствующем сообщении.
7 Примечание
Возможно, вы не сможете вводить десятичные запятые в полях для
десятичных чисел. Чтобы обеспечить поддержку проверки jQuery
для
других языков, кроме английского, используйте вместо десятичной точки
запятую (","), а для отображения данных в форматах для других языков, кроме
английского, выполните действия, необходимые для глобализации вашего
приложения. Инструкции по добавлению десятичной запятой см. в
комментарии GitHub 4076 .
Обратите внимание, что для каждого поля, содержащего недопустимое значение, в
форме автоматически отображается сообщение об ошибке проверки. Эти ошибки
применяются как на стороне клиента (с помощью JavaScript и jQuery), так и на
стороне сервера (если пользователь отключает JavaScript).
Основным преимуществом является то, что на страницах создания или
редактирования не требуется изменять код. Пользовательский интерфейс
проверки активируется после применения к модели атрибутов проверки
достоверности. В Razor Pages, создаваемом в рамках этого руководства, правила
проверки применяются автоматически (для этого к свойствам класса модели Movie
применяются атрибуты проверки). При проверке страницы редактирования
применяются те же правила.
Данные формы передаются на сервер только после того, как будут устранены все
ошибки проверки на стороне клиента. Чтобы убедиться, что данные формы не
отправляются, используйте любой из следующих способов.
Поместите точку останова в метод OnPostAsync . Отправьте форму, выбрав
Создать или Сохранить. Точка останова не достигается ни при каких
обстоятельствах.
Используйте инструмент Fiddler .
Проследите сетевой трафик с помощью инструментов разработчика для
браузера.
Проверка на стороне сервера
Если в браузере отключен JavaScript, форма с ошибками отправляется на сервер.
Реализация проверки на стороне сервера:
1. Отключите JavaScript в браузере. JavaScript можно отключить с помощью
средств разработчика в браузере. Если JavaScript невозможно отключить в
этом браузере, попробуйте использовать другой браузер.
2. Поместите точку останова в метод OnPostAsync страниц создания или
редактирования.
3. Отправьте форму с недопустимыми данными.
4. Проверка недопустимого состояния модели:
C#
if (!ModelState.IsValid)
{
return Page();
}
Иначе отключите проверку на стороне клиента на сервере.
В следующем коде демонстрируется часть страницы Create.cshtml , созданной
ранее в рамках этого руководства. Она используется на страницах создания и
редактирования для выполнения следующих действий:
Отображения начальной формы.
Повторного отображения формы в случае ошибки.
CSHTML
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
Вспомогательная функция тега Input использует атрибуты DataAnnotations и
создает HTML-атрибуты, необходимые для проверки jQuery на стороне клиента.
Вспомогательная функция тега Validation отображает ошибки проверки.
Дополнительные сведения см. в разделе Проверка.
На страницах создания и редактирования не определены правила проверки.
Правила проверки и строки ошибок указываются только в классе Movie . Они
автоматически применяются к Razor Pages, которые редактируют модель Movie .
Любые необходимые изменения логики проверки осуществляются исключительно
в модели. Проверка применяется согласованно во всем приложении, логика
проверки определяется в одном месте. Такой подход позволяет максимально
оптимизировать код и упростить его поддержку и обновление.
Использование атрибутов DataType
Проверьте класс Movie . В пространстве имен System.ComponentModel.DataAnnotations
в дополнение к набору встроенных атрибутов проверки предоставляются атрибуты
форматирования. Атрибут [DataType] применяется к свойствам ReleaseDate и
Price .
C#
// [Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
Атрибуты [DataType] предоставляют:
Указания для обработчика представлений для форматирования данных.
Атрибуты, например <a> для URL-адресов и <a
href="mailto:EmailAddress.com"> для электронной почты.
Используйте атрибут [RegularExpression] для проверки формата данных. Атрибут
[DataType] позволяет указать тип данных с более точным определением по
сравнению со встроенным типом базы данных. Атрибуты [DataType] не
предназначены для проверки. В примере приложения отображается только дата
без времени.
Перечисление DataType предоставляет множество типов данных, таких как Date ,
Time , PhoneNumber , Currency , EmailAddress и т. д.
Атрибуты [DataType] :
Может включить автоматическое предоставление приложению функций для
конкретных типов. Например, можно создавать ссылку mailto: для
DataType.EmailAddress .
Могут предоставить селектор даты DataType.Date в браузерах,
поддерживающих HTML5.
Создают атрибуты HTML 5 data- , которые используются браузерами с
поддержкой HTML 5.
Не предназначены для проверки.
DataType.Date не задает формат отображаемой даты. По умолчанию поле данных
отображается с использованием форматов, установленных в параметрах
CultureInfo сервера.
Требуются заметки к данным [Column(TypeName = "decimal(18, 2)")] , чтобы Entity
Framework Core корректно сопоставила Price с валютой в базе данных.
Дополнительные сведения см. в разделе Типы данных.
С помощью атрибута [DisplayFormat] можно явно указать формат даты:
C#
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =
true)]
public DateTime ReleaseDate { get; set; }
Параметр ApplyFormatInEditMode указывает, что при отображении значения для
редактирования будет применяться форматирование. Это поведение может быть
нежелательным для некоторых полей. Например, в полях валюты в
пользовательском интерфейсе редактирования использовать символ денежной
единицы, как правило, не требуется.
Атрибут [DisplayFormat] может использоваться отдельно, однако чаще всего его
рекомендуется применять вместе с атрибутом [DataType] . Атрибут [DataType]
передает семантику данных (в отличие от способа их вывода на экран). Атрибут
[DataType] дает следующие преимущества, недоступные в [DisplayFormat] :
Поддержка функций HTML5 в браузере, например отображение элемента
управления календарем, соответствующего языковому стандарту символа
валюты, ссылок электронной почты и т. д.
По умолчанию формат отображения данных в браузере определяется в
соответствии с установленным языковым стандартом.
Атрибут [DataType] обеспечивает поддержку платформы ASP.NET Core для
выбора соответствующего шаблона поля, применяемого при отображении
данных. При отдельном использовании атрибут DisplayFormat базируется на
строковом шаблоне.
Примечание. Проверка jQuery не работает с атрибутом [Range] и DateTime .
Например, следующий код всегда приводит к возникновению ошибки проверки на
стороне клиента, даже если дата попадает в указанный диапазон:
C#
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
Лучшей методикой будет не компилировать модели с фиксированными датами,
поэтому использовать атрибуты [Range] и DateTime следует крайне осторожно.
Используйте конфигурацию для диапазонов дат и других значений, подверженных
частым изменениям, а не указывайте их в коде.
В следующем коде демонстрируется объединение атрибутов в одной строке:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RazorPagesMovie.Models;
public class Movie
{
public int Id { get; set; }
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; } = string.Empty;
[Display(Name = "Release Date"), DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
public string Genre { get; set; } = string.Empty;
[Range(1, 100), DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; } = string.Empty;
}
Начало работы с Razor Pages и EF Core показывает расширенные EF Core операции
со Razor Страницами.
Применение миграции
DataAnnotation, примененные к классу, изменяют схему. Например, DataAnnotation,
примеренные к полю Title , выполняют следующее:
C#
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; } = string.Empty;
ограничивают число символов до 60;
не допускают значение null .
Сейчас таблица Movie имеет следующую схему:
SQL
CREATE TABLE [dbo].[Movie] (
[ID]
INT
[Title]
NVARCHAR (MAX)
[ReleaseDate] DATETIME2 (7)
[Genre]
NVARCHAR (MAX)
[Price]
DECIMAL (18, 2)
[Rating]
NVARCHAR (MAX)
CONSTRAINT [PK_Movie] PRIMARY
);
IDENTITY (1, 1) NOT NULL,
NULL,
NOT NULL,
NULL,
NOT NULL,
NULL,
KEY CLUSTERED ([ID] ASC)
Предыдущие изменения схемы не приводят к созданию исключения EF. Но следует
создать миграцию, чтобы схема соответствовала модели.
Visual Studio
В меню Сервис последовательно выберите пункты Диспетчер пакетов NuGet
> Консоль диспетчера пакетов. В PMC введите следующие команды:
PowerShell
Add-Migration New_DataAnnotations
Update-Database
Update-Database выполняет методы Up класса New_DataAnnotations . Проверьте
метод Up .
C#
public partial class NewDataAnnotations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Movie",
type: "nvarchar(60)",
maxLength: 60,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.AlterColumn<string>(
name: "Rating",
table: "Movie",
type: "nvarchar(5)",
maxLength: 5,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
migrationBuilder.AlterColumn<string>(
name: "Genre",
table: "Movie",
type: "nvarchar(30)",
maxLength: 30,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
}
Обновленная таблица Movie имеет следующую схему:
SQL
CREATE TABLE [dbo].[Movie] (
[ID]
INT
[Title]
NVARCHAR (60)
[ReleaseDate] DATETIME2 (7)
[Genre]
NVARCHAR (30)
[Price]
DECIMAL (18, 2)
[Rating]
NVARCHAR (5)
CONSTRAINT [PK_Movie] PRIMARY
);
IDENTITY (1, 1) NOT NULL,
NOT NULL,
NOT NULL,
NOT NULL,
NOT NULL,
NOT NULL,
KEY CLUSTERED ([ID] ASC)
Публикация в Azure
Сведения о развертывании в Azure, см. в разделе Учебник: Создание приложения
ASP.NET Core в Azure с подключением к базе данных SQL.
Благодарим вас за изучение общих сведений о страницах Razor. Начало работы с
Razor Pages и EF Core является отличным продолжением работы с этим
руководством.
Дополнительные ресурсы
Вспомогательные функции тегов в формах в ASP.NET Core
Глобализация и локализация в ASP.NET Core
Вспомогательные функции тегов в ASP.NET Core
Создание вспомогательных функций тегов в ASP.NET Core
Предыдущая статья. Добавление нового поля
Начало работы с MVC ASP.NET Core
Статья • 15.11.2022 • Чтение занимает 17 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом руководстве описывается веб-разработка MVC ASP.NET Core с
контроллерами и представлениями. Если вы не знакомы с веб-разработкой
ASP.NET Core, для начала изучите версию этого руководства для Razor Pages. См.
статью Выбор пользовательского интерфейса ASP.NET Core, где сравниваются Razor
Pages, MVC и Blazor для разработки пользовательского интерфейса.
Это первое руководство из серии материалов по веб-разработке MVC ASP.NET
Core с использованием контроллеров и представлений.
Пройдя всю серию, вы создадите приложение, которое управляет базой данных
фильмов и отображает ее. Вы научитесь:
" Создание веб-приложения.
" Добавление модели и формирование шаблона.
" Работа с базой данных.
" Добавление поиска и проверки.
Просмотреть или скачать пример кода
(описание скачивания).
Предварительные требования
Visual Studio
Последняя предварительная версия Visual Studio 2022
с рабочей
нагрузкой ASP.NET и веб-разработка .
Создание веб-приложения
Visual Studio
Запустите Visual Studio и щелкните Создать проект
В диалоговом окне Создание проекта выберите Веб-приложение
ASP.NET Core (Модель — представление — контроллер)>Далее.
В диалоговом окне Настроить новый проект введите MvcMovie в поле
Имя проекта. Важно присвоить проекту имя MvcMovie. Регистр символов
должен соответствовать каждому экземпляру namespace при копировании
кода.
Выберите Далее.
В диалоговом окне Дополнительные сведения выполните следующие
действия.
Выберите .NET 7.0.
Убедитесь, что флажок Не использовать операторы верхнего уровня
снят.
Нажмите кнопку создания.
Дополнительные сведения, включая альтернативные подходы к созданию
проекта, см. в статье Создание проекта в Visual Studio.
В Visual Studio используется шаблон проекта по умолчанию для созданного
проекта MVC. Созданный проект это:
рабочее приложение;
простой начальный проект.
Запуск приложения
Visual Studio
Нажмите клавиши CTRL+F5, чтобы запустить приложение без отладчика.
Visual Studio отображает следующее диалоговое окно, если проект еще не
настроен для использования SSL:
Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.
Отобразится следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата
браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio запускает приложение и открывает браузер по умолчанию.
В адресной строке указывается localhost:<port#> , а не что-либо типа
example.com . Стандартное имя узла для локального компьютера — localhost .
Когда Visual Studio создает веб-проект, для веб-сервера используется
случайный порт.
Запуск приложения без отладки путем нажатия клавиш CTRL+F5 позволяет:
Внесите изменения в код.
Сохраните файл.
Быстро обновить браузер и просмотреть изменения в коде.
Из меню Отладка можно запустить приложение с отладкой или без:
Вы можете выполнить отладку приложения, нажав кнопку HTTPS на панели
инструментов:
Пример приложения приведен на следующем рисунке:
Visual Studio
Справка по Visual Studio
Сведения об отладке кода C# с помощью Visual Studio
Введение в интегрированную среду разработки Visual Studio
В следующем руководстве этой серии вы узнаете о MVC и о том, как начать писать
код.
Далее: Добавление контроллера
Часть 2. Добавление контроллера в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 15 мин
Автор: Рик Андерсон
(Rick Anderson)
Модель архитектуры MVC разделяет приложение на три основных компонента:
Model (модель), View (представление) и Controller (контроллер). С помощью
модели MVC можно создавать приложения, которые удобнее тестировать и
обновлять по сравнению с традиционными монолитными приложениями.
Приложения на основе модели MVC содержат следующее:
Модели: классы, представляющие данные в приложении. Классы модели
используют логику проверки, которая позволяет применять бизнес-правила к
этим данным. Как правило, объекты модели извлекают и сохраняют состояние
модели в базе данных. В этом руководстве модель Movie извлекает сведения о
фильмах из базы данных и передает их в представление или обновляет.
Обновленные данные записываются в базу данных.
Представления: компоненты, которые формируют пользовательский
интерфейс приложения. Как правило, в пользовательском интерфейсе
отображаются данные модели.
Контроллеры: классы, которые:
обрабатывают запросы браузера;
получают данные модели;
вызывают шаблоны представления вызовов, которые возвращают ответ.
В приложении MVC представление используется только для отображения
информации. Контроллер обрабатывает и реагирует на ввод и
взаимодействие пользователя. Например, контроллер обрабатывает сегменты URLадреса и значения строки запроса и передает эти значения в модель. Модель
может использовать эти значения для выполнения запросов к базе данных.
Пример.
https://localhost:5001/Home/Privacy : задает контроллер Home и действие
Privacy .
https://localhost:5001/Movies/Edit/5 : это запрос на изменение фильма с ID=5
с помощью контроллера Movies и действия Edit , которые подробно описаны
далее в этом руководстве.
Данные маршрута описаны далее в этом руководстве.
Структура архитектуры MVC разделяет приложение на три основных группы
компонентов: модели, представления и контроллеры. Этот шаблон помогает
реализовать принципы разделения задач: логика пользовательского интерфейса
относится к представлению. Логика ввода относится к контроллеру. Бизнес-логика
размещается в модели. Подобное разделение позволяет эффективно справляться с
трудностями при построении приложения, поскольку оно дает возможность
работать одновременно с одним аспектом реализации, не затрагивая при этом код
других. Например, вы можете изменять код представления независимо от кода
бизнес-логики.
Эти принципы продемонстрированы в этой серии руководств при создании
приложения для работы с фильмами. Проект MVC содержит папки для
контроллеров и представлений.
Добавление контроллера
Visual Studio
В Обозревателе решений щелкните правой кнопкой мыши Контроллеры >
Добавить > Контроллер.
В диалоговом окне Добавление нового шаблонного элемента выберите
Контроллер MVC — пустой>Добавить.
В диалоговом окне Добавление нового элемента — MvcMovie введите
HelloWorldController.cs и щелкните Добавить.
Замените все содержимое Controllers/HelloWorldController.cs следующим кодом:
C#
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers;
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/
public string Index()
{
return "This is my default action...";
}
//
// GET: /HelloWorld/Welcome/
public string Welcome()
{
return "This is the Welcome action method...";
}
}
Каждый метод public в контроллере вызывается как конечная точка HTTP. В
приведенном выше примере оба метода возвращают строку. Обратите внимание
на комментарии, предшествующие каждому методу.
Конечная точка HTTP:
Это URL-адрес, который является целевым в веб-приложении, например
https://localhost:5001/HelloWorld .
Она объединяет:
используемый протокол: HTTPS ;
сетевое расположение веб-сервера, включая порт TCP: localhost:5001 ;
целевой универсальный код ресурса (URI): HelloWorld .
В первом комментарии указано, что этот метод HTTP GET
вызывается путем
добавления /HelloWorld/ к базовому URL-адресу.
Во втором комментарии указано, что этот метод HTTP GET
вызывается путем
добавления /HelloWorld/Welcome/ к URL-адресу. Далее в этом руководстве
используется механизм формирования шаблонов для создания методов HTTP POST ,
которые обновляют данные.
Запустите приложение без отладчика.
Добавьте "HelloWorld" к пути в адресной строке. Метод Index возвращает строку.
MVC вызывает классы контроллера (и методы действия в них) в зависимости от
входящего URL-адреса. Логика маршрутизации URL-адресов, используемая
моделью MVC по умолчанию, определяет вызываемый код на основе формата
следующего вида:
/[Controller]/[ActionName]/[Parameters]
Формат маршрутизации задается в файле Program.cs .
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Если при переходе к приложению не указать сегменты URL-адреса, по умолчанию
устанавливаются контроллер Home и метод Index, которые заданы в выделенной
выше строке шаблона. В предшествующих сегментах URL-адресов:
Первый сегмент URL-адреса определяет класс контроллера, который будет
выполняться. Поэтому localhost:5001/HelloWorld сопоставляется с классом
Controller HelloWorld.
Вторая часть сегмента URL-адреса определяет метод действия для класса.
Таким образом, localhost:5001/HelloWorld/Index выполняет метод Index
класса HelloWorldController . Обратите внимание, что в этом случае
достаточно перейти по адресу localhost:5001/HelloWorld , а метод Index
вызывается по умолчанию. Если имя вызываемого метода не указано явно,
для контроллера вызывается метод по умолчанию Index .
В третьей части сегмента URL-адреса ( id ) указываются данные маршрута.
Данные маршрута описаны далее в этом руководстве.
Перейдите по адресу https://localhost:{PORT}/HelloWorld/Welcome . Замените {PORT}
на номер порта.
Метод Welcome запускается и возвращает строку This is the Welcome action
method... . Для этого URL-адреса заданы контроллер HelloWorld и метод действия
Welcome . Часть URL-адреса [Parameters] на данный момент еще не использовалась.
Измените код, чтобы передать сведения о параметрах из URL-адреса в контроллер.
Например, /HelloWorld/Welcome?name=Rick&numtimes=4 .
Измените метод Welcome , включив два параметра, как показано в следующем коде.
C#
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is:
{numTimes}");
}
Предыдущий код:
Использует функцию необязательного параметра C#, указывая, что параметр
numTimes по умолчанию принимает значение 1, если ему не было передано
значение.
Использует HtmlEncoder.Default.Encode для защиты приложения от
злонамеренного ввода данных (например, JavaScript).
Использует интерполированные строки в $"Hello {name}, NumTimes is:
{numTimes}" .
Запустите приложение и перейдите по адресу https://localhost:
{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4 . Замените {PORT} на номер порта.
Попробуйте использовать разные значения для name и numtimes в URL-адресе.
Система привязки модели MVC автоматически сопоставляет именованные
параметры из строки запроса с параметрами метода. Дополнительные сведения
см. в разделе Привязка модели.
На предыдущем рисунке:
Сегмент URL-адреса Parameters не используется.
Параметры name и numTimes передаются в строку запроса .
Вопросительный знак ( ? ) в приведенном выше URL-адресе используется в
качестве разделителя, после которого указывается строка запроса.
Символом & отделяются пары "поле-значение".
Замените метод Welcome следующим кодом:
C#
public string Welcome(string name, int ID = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}
Запустите приложение и введите следующий URL-адрес: https://localhost:
{PORT}/HelloWorld/Welcome/3?name=Rick
В предыдущем URL-адресе:
Третий сегмент URL-адреса сопоставляется с параметром маршрута id .
Метод Welcome содержит параметр id , который сопоставляется с шаблоном
URL-адреса в методе MapControllerRoute .
Завершающий символ ? указывает на начало строки запроса
.
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
В предшествующем примере:
Третий сегмент URL-адреса сопоставляется с параметром маршрута id .
Метод Welcome содержит параметр id , который сопоставляется с шаблоном
URL-адреса в методе MapControllerRoute .
Завершающий символ ? (в id? ) указывает, что параметр id является
необязательным.
Назад: Приступая к работе
Далее: Добавление представления
Часть 3. Добавление представления в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 20 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом разделе вы внесете изменения в класс HelloWorldController для
использования файлов представления Razor. Это позволяет аккуратно
инкапсулировать процесс создания HTML-ответов в клиент.
Шаблоны представлений создаются с помощью Razor. Шаблоны представлений на
основе Razor:
имеют расширение файла .cshtml ;
реализуют удобный способ для создания выходных данных HTML с помощью
C#.
Сейчас метод Index возвращает строку с сообщением в классе контроллера. В
классе HelloWorldController замените метод Index следующим кодом:
C#
public IActionResult Index()
{
return View();
}
Предыдущий код:
вызывает метод View контроллера;
использует шаблон представления для создания HTML-ответа.
Методы контроллера:
называются методами действий; например, метод действия Index в
предыдущем коде;
обычно возвращают IActionResult или класс, производный от ActionResult, а не
тип, например string .
Добавление представления
Visual Studio
Щелкните правой кнопкой мыши папку Views, а затем выберите Добавить >
Новая папка. Назовите папку HelloWorld.
Щелкните правой кнопкой мыши папку Views/HelloWorld, а затем выберите
Добавить > Новый элемент.
В диалоговом окне Добавление нового элемента MvcMovie выполните
следующие действия.
В поле поиска в правом верхнем углу введите view (представление).
Выберите Представление Razor — пустое
В поле Index.cshtml оставьте значение Index.cshtml.
Нажмите Добавить
Замените содержимое файла представления Views/HelloWorld/Index.cshtml Razor
следующим.
CSHTML
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>Hello from our View Template!</p>
Перейдите к https://localhost:{PORT}/HelloWorld :
Метод Index в HelloWorldController выполнил оператор return View(); ,
который указал, что метод должен использовать файл шаблона
представления для отображения ответа в браузере.
Имя файла шаблона представления не указано, MVC по умолчанию
использует файл представления по умолчанию. Если имя файла
представления не указано, возвращается представление по умолчанию. В
этом примере представление по умолчанию имеет то же имя, что и метод
действия Index . Используется шаблон представления
/Views/HelloWorld/Index.cshtml .
На изображении ниже показана строка "Hello from our View Template!",
которая жестко задана в представлении:
Изменение представлений и страниц макета
Выберите ссылки в меню (MvcMovie, Home и Privacy ). Меню на каждой странице
имеют одинаковый макет. Макет меню реализован в файле
Views/Shared/_Layout.cshtml .
Откройте файл Views/Shared/_Layout.cshtml .
Шаблоны макета позволяют:
указать макет контейнера HTML сайта в одном месте;
применять макета контейнера HTML на нескольких страницах сайта.
Найдите строку @RenderBody() . RenderBody — это заполнитель, в котором
отображаются все создаваемые страницы для определенных представлений,
упакованные на странице макета. Например, если щелкнуть ссылку Privacy,
представление Views/Home/Privacy.cshtml отобразится в методе RenderBody .
Изменение заголовка, нижнего колонтитула
и ссылки меню в файле макета
Замените содержимое файла Views/Shared/_Layout.cshtml следующей разметкой:
Изменения выделены:
CSHTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Movie App</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"
/>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbarlight bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Movies"
asp-action="Index">Movie App</a>
<button class="navbar-toggler" type="button" data-bstoggle="collapse" data-bs-target=".navbar-collapse" ariacontrols="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2022 - Movie App - <a asp-area="" asp-controller="Home"
asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Приведенная выше разметка вносит следующие изменения:
замена трех вхождений MvcMovie на Movie App ;
Замена элемента привязки <a class="navbar-brand" asp-area="" aspcontroller="Home" asp-action="Index">MvcMovie</a> на <a class="navbar-brand"
asp-controller="Movies" asp-action="Index">Movie App</a> .
В приведенной выше разметке атрибут вспомогательной функции тега
привязки asp-area="" и значение атрибута были опущены, так как это приложение
не использует области (Areas).
Примечание. Контроллер Movies не был реализован. На этом этапе ссылка Movie
App не работает.
Сохраните изменения и щелкните ссылку Privacy . Обратите внимание, что
заголовок вкладки браузера отображает Privacy Политика — Приложение Movie
вместо Privacy Политика — MvcMovie.
Выберите ссылку Home.
Обратите внимание, что в заголовке и тексте привязки также отображается Movie
App. Внеся одно изменение в шаблон макета, мы изменили заголовок и текст
ссылки на всех страницах сайта.
Просмотрите файл Views/_ViewStart.cshtml .
CSHTML
@{
Layout = "_Layout";
}
Файл Views/_ViewStart.cshtml предоставляет файл Views/Shared/_Layout.cshtml для
каждого представления. Свойство Layout может задавать другое представление
макета или иметь значение null , при котором макет не используется.
Откройте файл представления Views/HelloWorld/Index.cshtml .
Измените заголовок и элемент <h2> , как выделено ниже:
CSHTML
@{
ViewData["Title"] = "Movie List";
}
<h2>My Movie List</h2>
<p>Hello from our View Template!</p>
Заголовок и элемент <h2> немного отличаются, чтобы вы видели, какой именно
фрагмент кода изменяет отображение.
ViewData["Title"] = "Movie List"; в приведенном выше коде присваивает свойству
Title словаря ViewData значение "Movie List". Свойство Title используется в
элементе HTML <title> на странице макета:
CSHTML
<title>@ViewData["Title"] - Movie App</title>
Сохраните изменения и перейдите к https://localhost:{PORT}/HelloWorld .
Обратите внимание, что были изменены следующие элементы:
Заголовок браузера.
Основной заголовок.
Дополнительные заголовки.
Если в браузере изменения не отображаются, это может означать, что содержимое
кэшировано. В этом случае нажмите в браузере клавиши CTRL+F5 для
принудительной загрузки ответа сервера. Заголовок браузера создается с
помощью атрибута ViewData["Title"] , который задается в шаблоне представления
Index.cshtml и дополнительной строки "- Movie App", добавляемой в файл макета.
Содержимое шаблона представления Index.cshtml объединяется с шаблоном
представления Views/Shared/_Layout.cshtml . В браузер отправляется один HTMLответ. Шаблоны макетов позволяют легко вносить изменения, применяемые ко
всем страницам в приложении. Дополнительные сведения см. в разделе Макет.
Небольшой фрагмент данных (сообщение "Hello from our View Template!") все
равно жестко задан в коде. Приложение MVC предоставляет представление, вы
реализуете контроллер, однако модели на данный момент еще нет.
Передача данных из контроллера в
представление
Действия контроллера вызываются в ответ на входящий запрос URL-адреса. Код,
обрабатывающий входящие запросы браузера, добавляется в класс контроллера.
Контроллер извлекает данные из источника данных и определяет тип ответа,
который будет отправлен в браузер. Контроллер может использовать шаблоны
представлений для создания и форматирования ответа HTML, отправляемого
браузеру.
Контроллер отвечает за предоставление данных, необходимых шаблону
представления для отображения ответа.
Шаблоны представлений недолжны:
выполнять бизнес-логику;
непосредственно взаимодействовать с базой данных.
Вместо этого они должны работать только с данными, которые им
предоставляет контроллер. Подобное разделение сфер ответственности позволяет
обеспечить максимальную оптимизацию кода, а также удобство его
очистки;
тестирования;
обслуживания.
Сейчас метод Welcome в классе HelloWorldController принимает параметры name и
ID , после чего выводит значения напрямую в браузер.
Вместо отображения ответа в виде строки настройте контроллер для
использования шаблона представления. Шаблон представления создает
динамический ответ, для получения которого необходимо передать
соответствующие данные из контроллера в представление. Для этого контроллер
может поместить динамические данные (параметры), которые требуются шаблону
представления, в словарь ViewData , к которому впоследствии будет обращаться
этот шаблон за динамическими данными.
В файле HelloWorldController.cs измените метод Welcome так, чтобы добавлять
значения Message и NumTimes в словарь ViewData .
Словарь ViewData представляет собой динамический объект, а это означает, что
можно использовать любой тип. Объект ViewData не имеет определенных свойств,
пока не будет добавлен какой-либо элемент. Система привязки модели MVC
автоматически сопоставляет именованные параметры name и numTimes из строки
запроса с параметрами метода. Полный HelloWorldController :
C#
using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
namespace MvcMovie.Controllers;
public class HelloWorldController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult Welcome(string name, int numTimes = 1)
{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;
return View();
}
}
Объект словаря ViewData содержит данные, которые будут передаваться в
представление.
Создайте шаблон представления экрана приветствия с именем
Views/HelloWorld/Welcome.cshtml .
В шаблоне представления Welcome.cshtml создайте цикл, который будет
отображать строку "Hello" NumTimes . Замените все содержимое
Views/HelloWorld/Welcome.cshtml следующим:
CSHTML
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
Сохраните изменения и перейдите по следующему URL-адресу:
https://localhost:{PORT}/HelloWorld/Welcome?name=Rick&numtimes=4
Данные извлекаются по URL-адресу и передаются в контроллер с помощью
средства привязки модели MVC. Контроллер упаковывает данные в словарь
ViewData и передает этот объект в представление. Представление отображает
данные в формате HTML в браузере.
В примере выше мы использовали словарь ViewData для передачи данных из
контроллера в представление. Далее в этом руководстве для передачи данных из
контроллера в представление мы будем использовать модель представления.
Подход к передаче данных на основе модели представления является
предпочтительным относительно применения словаря ViewData .
В следующем руководстве создается база данных фильмов.
Назад: Добавление контроллера
Далее: Добавление модели
Часть 4. Добавление модели в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 51 мин
Авторы: Рик Андерсон
(Rick Anderson) и Джон П. Смит
(Jon P Smith)
В этом учебнике добавляются классы для управления фильмами в базе данных. Эти
классы представляет уровень модели в приложении MVC.
Эти классы моделей используются с Entity Framework Core (EF Core) для работы с
базой данных. EF Core — это платформа объектно-реляционного сопоставления
(ORM), которая упрощает написание кода доступа к данным.
Созданные классы модели известны как классы POCO (Plain Old CLR Objects —
простые старые объекты CLR). Классы POCO не имеют никакой зависимости EF
Coreот . Эти классы только определяют свойства данных, которые хранятся в базе
данных.
В этом руководстве сначала создаются классы моделей и EF Core создается база
данных.
Добавление класса модели данных
Visual Studio
Щелкните правой кнопкой мыши папку Models и выберите Добавить>Class
(Класс). Задайте файлу имя Movie.cs .
Обновите файл Models/Movie.cs , используя следующий код:
C#
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models;
public class Movie
{
public int Id { get; set; }
public string? Title { get; set; }
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
public decimal Price { get; set; }
}
Класс Movie содержит поле Id , которое требуется базе данных в качестве
первичного ключа.
Атрибут DataType для ReleaseDate указывает тип данных ( Date ). С этим атрибутом:
Пользователю не требуется вводить сведения о времени в поле даты.
Отображается только дата, а не время.
DataAnnotations рассматриваются в следующем руководстве.
Знак вопроса после string указывает, что свойство допускает значение NULL.
Дополнительные сведения см. в статье Ссылочные типы, допускающие значение
NULL.
Добавление пакетов NuGet
Visual Studio
В меню Сервис последовательно выберите пункты Диспетчер пакетов
NuGet>Консоль диспетчера пакетов (PMC).
В PMC выполните следующие команды:
PowerShell
Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Приведенные выше команды добавляют следующие компоненты:
Поставщик EF Core SQL Server. Пакет поставщика устанавливает пакет в EF
Core качестве зависимости.
Программы, используемые пакетами, устанавливаются автоматически на
этапе формирования шаблонов далее в этом учебнике.
Выполните сборку проекта, чтобы проверить его на ошибки компиляции.
Формирования шаблона страниц фильмов
Используйте средство формирования шаблонов, чтобы создать страницы Create ,
Read , Update и Delete (CRUD) для модели фильма.
Visual Studio
В Обозревателе решений щелкните правой кнопкой мыши папку Controllers и
выберите Добавить > New Scaffolded Item (Создать шаблонный элемент).
В диалоговом окне Добавление нового элемента с шаблоном выберите
Контроллер MVC с представлениями, используя команду Добавить Entity
Framework>.
В диалоговом окне добавления контроллера MVC с представлениями с
использованием Entity Framework выполните следующее.
В раскрывающемся списке Класс модели выберите Фильм
(MvcMovie.Models) .
В строке Класс контекста данных щелкните знак плюса (+).
В диалоговом окне Добавление контекста данных создается имя
класса MvcMovie.Data.PagesMovieContext.
Выберите Добавить.
Представления и Имя контроллера: оставьте значения по умолчанию.
Выберите Добавить.
Если отобразится сообщение об ошибке, выберите Добавить еще раз, чтобы
повторить попытку.
При формировании шаблонов выполняются такие действия:
вставка необходимых ссылок на пакеты в файл проекта MvcMovie.csproj ;
регистрация контекста базы данных в файле Program.cs .
добавление строки подключения к базе данных в файл appsettings.json .
При формировании шаблонов выполняется создание таких объектов:
контроллер фильмов Controllers/MoviesController.cs ;
файлы представления Razor для страниц Create, Delete, Details, Edit и Index:
Views/Movies/*.cshtml ;
класс контекста для базы данных: Data/MvcMovieContext.cs .
Автоматическое создание этих файлов и их обновление называется
формированием шаблонов.
Сформированные страницы пока использовать нельзя, так как база данных не
существует. Запуск приложения и выбор ссылки Movie App приводит к
возникновению сообщения об ошибке Не удается открыть базу данных или no
such table: Movie (Такая таблица отсутствует: Movie).
Выполните сборку приложения, чтобы убедиться в отсутствии ошибок.
Первоначальная миграция
Используйте функцию EF CoreМиграции для создания базы данных. Миграции —
это набор средств, с помощью которых можно создавать и обновлять базы данных
в соответствии с моделью данных.
Visual Studio
В меню Сервис последовательно выберите пункты Диспетчер пакетов
NuGet>Консоль диспетчера пакетов.
Введите в консоли диспетчера пакетов (PMC) следующие команды:
PowerShell
Add-Migration InitialCreate
Update-Database
Add-Migration InitialCreate . Создает файл миграции
Migrations/{timestamp}_InitialCreate.cs . Аргумент InitialCreate — это
имя миграции. Можно использовать любое имя, но по соглашению
выбирается имя, которое описывает миграцию. Так как это первая
миграция, созданный класс содержит код для создания схемы базы
данных. Схема базы данных создается на основе модели, указанной в
классе MvcMovieContext .
Update-Database . Обновляет базу данных до последней миграции,
созданной предыдущей командой. Эта команда выполняет метод Up в
файле Migrations/{time-stamp}_InitialCreate.cs , который создает базу
данных.
Команда Update-Database выдает следующее предупреждение:
"Для десятичного столбца Price в типе сущности Movie не указан тип. Это
приведет к тому, что значения будут усекаться без вмешательства
пользователя, если они не помещаются в значения точности и масштаба
по умолчанию. С помощью метода HasColumnType() явно укажите тип
столбца SQL Server, который может вместить все значения".
Игнорируйте предыдущее предупреждение, оно уже исправлено в следующем
учебнике.
Дополнительные сведения о средствах PMC для EF Coreсм. в справочникеEF
Core по инструментам — PMC в Visual Studio.
Тестирование приложения
Запустите приложение и выберите ссылку Movie App.
Если вы получили исключение, похожее на следующее, возможно, вы пропустили
dotnet ef database update команду на шаге миграции:
Visual Studio
Консоль
SqlException: Cannot open database "MvcMovieContext-1" requested by the
login. The login failed.
7 Примечание
В поле Price нельзя вводить десятичные запятые. Чтобы обеспечить
поддержку проверки jQuery
для других языков, кроме английского,
используйте вместо десятичной точки запятую (,), а для отображения данных в
форматах для других языков, кроме английского, выполните глобализацию
приложения. Инструкции по глобализации см. на сайте GitHub
.
Изучение созданного класса контекста базы данных, а
также регистрации
При EF Coreиспользовании доступ к данным осуществляется с помощью модели.
Модель состоит из классов сущностей и объекта контекста, который представляет
сеанс взаимодействия с базой данных. Объект контекста позволяет выполнять
запросы и сохранять данные. Контекст базы данных наследуется от
Microsoft.EntityFrameworkCore.DbContext и определяет сущности, которые
необходимо включить в модель данных.
При формировании шаблонов создается класс контекста базы данных
Data/MvcMovieContext.cs :
C#
using
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
Microsoft.EntityFrameworkCore;
MvcMovie.Models;
namespace MvcMovie.Data
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}
public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
}
}
Приведенный выше код создает свойство DbSet<Movie>, представляющее фильмы
в базе данных.
Внедрение зависимостей
ASP.NET Core поддерживает внедрение зависимостей. Службы, такие как контекст
базы данных, регистрируются с помощью DI в Program.cs . Эти службы
предоставляются компонентам, которые запрашивают их через параметры
конструктора.
В файле Controllers/MoviesController.cs этот конструктор применяет внедрение
зависимостей для внедрения контекста базы данных MvcMovieContext в контроллер.
Контекст базы данных используется в каждом методе создания, чтения, обновления
и удаления
в контроллере.
При формировании шаблонов был создан следующий выделенный код в
Program.cs :
Visual Studio
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
Система конфигурации ASP.NET Core считывает строку подключения к базе данных
MvcMovieContext.
Проверка созданной строки подключения к базе
данных
При формировании шаблонов в файл appsettings.json была добавлена строка
подключения:
Visual Studio
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MvcMovieContext": "Server=
(localdb)\\mssqllocaldb;Database=MvcMovieContext7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
При локальной разработке система конфигурации ASP.NET Core считывает ключ
ConnectionString из файла appsettings.json .
Класс InitialCreate
Изучите файл миграции Migrations/{timestamp}_InitialCreate.cs .
C#
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MvcMovie.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Movie",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(max)",
nullable: true),
ReleaseDate = table.Column<DateTime>(type: "datetime2",
nullable: false),
Genre = table.Column<string>(type: "nvarchar(max)",
nullable: true),
Price = table.Column<decimal>(type: "decimal(18,2)",
nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Movie", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Movie");
}
}
}
В приведенном выше коде:
метод InitialCreate.Up создает таблицу Movie и настраивает Id в качестве
первичного ключа;
метод InitialCreate.Down отменяет изменения схемы, внесенные при
миграции Up .
Внедрение зависимостей в контроллере
Откройте файл Controllers/MoviesController.cs и изучите его конструктор:
C#
public class MoviesController : Controller
{
private readonly MvcMovieContext _context;
public MoviesController(MvcMovieContext context)
{
_context = context;
}
Этот конструктор применяет внедрение зависимостей для внедрения контекста
базы данных ( MvcMovieContext ) в контроллер. Контекст базы данных используется в
каждом методе создания, чтения, обновления и удаления
в контроллере.
Протестируйте страницу создания. Введите и отправьте данные.
Протестируйте страницы редактирования, сведений и удаления.
Строго типизированные модели и директива
@model
Ранее в этом руководстве вы ознакомились с передачей данных или объектов из
контроллера в представление с использованием словаря ViewData . Словарь
ViewData представляет собой динамический объект, который реализует удобный
механизм позднего связывания для передачи информации в представление.
Модель MVC поддерживает передачу строго типизированных объектов модели в
представление. Такой подход обеспечивает проверку кода во время компиляции.
Механизм формирования шаблонов передал строго типизированную модель в
класс MoviesController и представления.
Изучите созданный метод Details в файле Controllers/MoviesController.cs :
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
Параметр id обычно передается в качестве данных маршрута. Например,
https://localhost:5001/movies/details/1 задает:
контроллер к контроллеру movies первый сегмент URL-адреса;
действие для details , второй сегмент URL-адреса;
значение 1 для id , последний сегмент URL-адреса.
Параметр id можно передать с помощью строки запроса, как показано в
следующем примере:
https://localhost:5001/movies/details?id=1
Параметр id определяется как тип, допускающий значение NULL ( int? ), в случае,
если не указано значение id .
Лямбда-выражение передается в метод FirstOrDefaultAsync для выбора записей
фильмов, которые соответствуют данным маршрута или значению строки запроса.
C#
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
Если фильм найден, экземпляр модели Movie передается в представление Details :
C#
return View(movie);
Изучите содержимое файла Views/Movies/Details.cshtml :
CSHTML
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.ReleaseDate)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.ReleaseDate)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Genre)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Genre)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.Price)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
Оператор @model в начале файла представления задает тип объекта, который будет
ожидаться представлением. При создании контроллера movie был включен
следующий оператор @model :
CSHTML
@model MvcMovie.Models.Movie
Эта директива @model обеспечивает доступ к фильму, который контроллер передал
в представление. Объект Model является строго типизированным. Например, в
представлении Details.cshtml код передает каждое поле фильма во
вспомогательные функции HTML DisplayNameFor и DisplayFor со строго
типизированным объектом Model . Методы Create и Edit и представления также
передают объект модели Movie .
Изучите представление Index.cshtml и метод Index в контроллере Movies. Обратите
внимание на то, как в коде создается объект List при вызове метода View . Код
передает список Movies из метода действия Index в представление:
C#
// GET: Movies
public async Task<IActionResult> Index(string searchString)
{
return _context.Movie != null ?
View(await _context.Movie.ToListAsync()) :
Problem("Entity set 'MvcMovieContext.Movie'
}
is null.");
Код возвращает сведения о проблеме , Movie если свойство контекста данных
имеет значение NULL.
При создании контроллера movies механизм формирования шаблонов включил
следующий оператор @model в начало файла Index.cshtml :
CSHTML
@model IEnumerable<MvcMovie.Models.Movie>
Эта директива @model обеспечивает доступ к списку фильмов, который контроллер
передал в представление с использованием строго типизированного объекта
Model . Например, в представлении Index.cshtml код циклически перебирает
фильмы с использованием оператора foreach для строго типизированного объекта
Model :
CSHTML
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a>
|
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Так как объект Model является строго типизированным (как и объект
IEnumerable<Movie> ), каждый элемент в цикле получает тип Movie . Помимо других
преимуществ, компилятор проверяет типы, используемые в коде.
Дополнительные ресурсы
Entity Framework Core для начинающих
Вспомогательные функции тегов
Глобализация и локализация
Назад: Добавление представления
Далее: Работа с SQL
Часть 5. Работа с базой данных в
приложении MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 12 мин
Авторы: Рик Андерсон
(Rick Anderson) и Джон П. Смит
(Jon P Smith)
Объект MvcMovieContext обрабатывает задачу подключения к базе данных и
сопоставления объектов Movie с записями базы данных. Контекст базы данных
регистрируется с помощью контейнера внедрения зависимостей в файле :
Visual Studio
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
Система конфигурации ASP.NET Core считывает ключ . Для разработки на
локальном уровне она получает строку подключения из файла
appsettings.json .
JSON
"ConnectionStrings": {
"MvcMovieContext": "Server=
(localdb)\\mssqllocaldb;Database=MvcMovieContext7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Если приложение развертывается на тестовом или рабочем сервере, можно задать
строку подключения к рабочему SQL Server с помощью переменной среды.
Дополнительные сведения см. в разделе Конфигурация.
Visual Studio
SQL Server Express LocalDB
LocalDB
— это упрощенная версия ядра СУБД SQL Server Express, устанавливаемая
по умолчанию с Visual Studio.
Запускается по требованию с помощью строки подключения.
Используется для разработки программ. Запускается в пользовательском
режиме, поэтому конфигурация не слишком сложная.
По умолчанию создает файлы .mdf в каталоге C:/Users/{user} .
Заполнение базы данных
Создайте класс SeedData в папке SeedData . Замените сгенерированный код
следующим кодом:
C#
using
using
using
using
using
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.DependencyInjection;
MvcMovie.Data;
System;
System.Linq;
namespace MvcMovie.Models;
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return;
// DB has been seeded
}
context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},
new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},
new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
Если в базе данных есть фильмы, возвращается инициализатор заполнения и
фильмы не добавляются.
C#
if (context.Movie.Any())
{
return; // DB has been seeded.
}
Добавление инициализатора заполнения
Visual Studio
Замените все содержимое Program.cs следующим кодом: Новый код выделен.
C#
using
using
using
using
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.DependencyInjection;
MvcMovie.Data;
MvcMovie.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovie
Context")));
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this
for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Удалите все записи в базе данных. Это можно сделать с помощью ссылок
удаления в браузере или из SSOX.
Тестирование приложения. Приложение должно выполнить инициализацию,
вызывая код в файле Program.cs , чтобы запустить метод заполнения. Чтобы
выполнить инициализацию принудительно, закройте окно командной строки,
открытое Visual Studio, и перезапустите, нажав клавиши CTRL+F5.
В приложении будут отображены данные.
Предыдущая статья — "Добавление модели"
Следующая статья — "Добавление методов и представлений контроллера"
Часть 6. Методы и представления
контроллера в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 23 мин
Автор: Рик Андерсон
(Rick Anderson)
Все готово для приложения по работе с фильмами, но презентация далеко не
идеальна, например, элемент ReleaseDate должен состоять из двух слов.
Откройте файл Models/Movie.cs и добавьте указанные ниже выделенные строки:
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
public class Movie
{
public int Id { get; set; }
public string? Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
}
DataAnnotations описаны в следующем учебнике. Атрибут Display определяет
отображаемое имя поля (в этом случае "Release Date" вместо "ReleaseDate").
Атрибут DataType определяет тип данных (Date), поэтому сведения о времени,
хранящиеся в поле, не отображаются.
Требуются заметки к данным [Column(TypeName = "decimal(18, 2)")] , чтобы Entity
Framework Core корректно сопоставила Price с валютой в базе данных.
Дополнительные сведения см. в разделе Типы данных.
Перейдите к контроллеру Movies . Наведите указатель мыши на ссылку Edit
(Изменить) и удерживайте его на месте, чтобы просмотреть целевой URL-адрес.
Ссылки Edit (Изменить), Details (Сведения) и Delete (Удалить) создаются
вспомогательной функцией тегов привязки Core MVC в файле
Views/Movies/Index.cshtml .
CSHTML
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
Вспомогательные функции тегов позволяют серверному коду участвовать в
создании и отображении HTML-элементов в файлах Razor. В представленном выше
коде AnchorTagHelper динамически создает значение атрибута HTML href на
основании метода действия контроллера и идентификатора маршрута. Для
изучения созданной разметки используйте функцию просмотра исходного кода в
вашем любимом браузере или средства для разработчика. Ниже показана часть
созданного кода HTML:
HTML
<td>
<a href="/Movies/Edit/4"> Edit </a> |
<a href="/Movies/Details/4"> Details </a> |
<a href="/Movies/Delete/4"> Delete </a>
</td>
Формат для routing задан в файле Program.cs :
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
ASP.NET Core преобразует https://localhost:5001/Movies/Edit/4 в запрос метода
действия Edit контроллера Movies с параметром Id , равным 4. (Методы
контроллера также называются методами действия.)
Вспомогательные функции тегов являются одной из самых популярных новых
возможностей в ASP.NET Core. Подробнее см. в разделе Дополнительные ресурсы.
Откройте контроллер Movies и изучите два метода действия Edit . В следующем
коде демонстрируется метод HTTP GET Edit , который выполняет выборку фильмов
и заполняет форму редактирования, созданную файлом Edit.cshtml Razor.
C#
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie.FindAsync(id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
В следующем коде показан метод HTTP POST Edit , который является владельцем
переданных значений фильмов:
C#
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you
want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
Атрибут [Bind] является одним из способов защиты от чрезмерной передачи
данных. Свойства необходимо включать только в тот атрибут [Bind] , который вы
хотите изменить. Дополнительные сведения см. в разделе Защита контроллера от
чрезмерной передачи данных. ViewModels
реализует альтернативный подход к
защите от чрезмерной передачи данных.
Обратите внимание на второй метод действия Edit , которому предшествует
атрибут [HttpPost] .
C#
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you
want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
Атрибут HttpPost указывает на то, что этот метод Edit может вызываться только
для запросов POST . Вы могли бы применить атрибут [HttpGet] к первому методу
редактирования, однако это необязательно, поскольку значение [HttpGet] задается
по умолчанию.
Атрибут ValidateAntiForgeryToken используется для защиты от подделки запросов и
используется совместно с соответствующим маркером безопасности, который
создается в файле представления редактирования ( Views/Movies/Edit.cshtml ). В
файле представления редактирования для создания маркера защиты от подделки
используется вспомогательная функция тега Form.
CSHTML
<form asp-action="Edit">
Вспомогательная функция тега Form создает скрытый маркер защиты от подделки,
который должен соответствовать [ValidateAntiForgeryToken] аналогичному
маркеру безопасности в методе Edit контроллера Movies. Дополнительные
сведения см. на странице Предотвращение атак с использованием подделки
межсайтовых запросов (XSRF/CSRF) в ASP.NET Core.
Метод HttpGet Edit принимает параметр фильма ID , выполняет поиск фильма с
использованием метода FindAsync платформы Entity Framework и возвращает
выбранный фильм в представление редактирования. Если фильм найти не удается,
возвращается ошибка NotFound (HTTP 404).
C#
// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie.FindAsync(id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
Если в представлении редактирования создана система формирования шаблонов,
она проверяет класс Movie и создает код для отображения элементов <label> и
<input> для каждого свойства класса. В следующем примере показано
представление редактирования, созданное системой формирования шаблонов
Visual Studio:
CSHTML
@model MvcMovie.Models.Movie
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Обратите внимание, что в начале файла шаблона представления содержится
оператор @model MvcMovie.Models.Movie . @model MvcMovie.Models.Movie указывает, что
в представлении требуется модель представления шаблона с типом Movie .
Для оптимизации разметки HTML сформированный код использует несколько
методов вспомогательных функций тегов. Вспомогательная функция тега Label
отображает имя поля ("Title", "ReleaseDate", "Genre" или "Price"). Вспомогательная
функция тега Input отображает элемент HTML <input> . Вспомогательная функция
тега Validation отображает любые сообщения проверки, связанные с указанным
свойством.
Запустите приложение и перейдите по URL-адресу /Movies . Щелкните ссылку Edit
(Изменить). Просмотрите исходный код страницы в окне браузера. Созданный
HTML-код для элемента <form> показан ниже.
HTML
<form action="/Movies/Edit/7" method="post">
<div class="form-horizontal">
<h4>Movie</h4>
<hr />
<div class="text-danger" />
<input type="hidden" data-val="true" data-val-required="The ID field
is required." id="ID" name="ID" value="7" />
<div class="form-group">
<label class="control-label col-md-2" for="Genre" />
<div class="col-md-10">
<input class="form-control" type="text" id="Genre"
name="Genre" value="Western" />
<span class="text-danger field-validation-valid" datavalmsg-for="Genre" data-valmsg-replace="true"></span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="Price" />
<div class="col-md-10">
<input class="form-control" type="text" data-val="true"
data-val-number="The field Price must be a number." data-val-required="The
Price field is required." id="Price" name="Price" value="3.99" />
<span class="text-danger field-validation-valid" datavalmsg-for="Price" data-valmsg-replace="true"></span>
</div>
</div>
<!-- Markup removed for brevity -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
<input name="__RequestVerificationToken" type="hidden"
value="CfDJ8Inyxgp63fRFqUePGvuI5jGZsloJu1L7X9le1gy7NCIlSduCRx9jDQClrV9pOTTmq
UyXnJBXhmrjcUVDJyDUMm7MF_9rK8aAZdRdlOri7FmKVkRe_2v5LIHGKFcTjPrWPYnc9AdSbomkiOSaTEg7RU" />
</form>
Элементы <input> находятся в элементе HTML <form> , атрибут action которого
задает передачу данных по URL-адресу /Movies/Edit/id . Данные формы будут
передаваться на сервер при нажатии кнопки Save . В последней строке перед
закрывающим элементом </form> отображается скрытый маркер XSRF, созданный
вспомогательной функцией тега Form.
Обработка запроса POST
В следующем листинге демонстрируется версия [HttpPost] метода действия Edit .
C#
// POST: Movies/Edit/5
// To protect from overposting attacks, enable the specific properties you
want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id,
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (id != movie.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(movie);
}
Атрибут [ValidateAntiForgeryToken] проверяет скрытый маркер безопасности XSRF,
который был создан генератором маркеров во вспомогательной функции тега
Form
Система модели привязки принимает переданные значения формы и создает
объект Movie , который передается в качестве параметра movie . Свойство
ModelState.IsValid проверяет, можно ли использовать переданные в форме
данные для изменения (редактирования или обновления) объекта Movie .
Допустимые данные сохраняются. Обновленные (измененные) данные фильма
сохраняются в базе данных посредством вызова метода SaveChangesAsync в
контексте базы данных. После сохранения данных код перенаправляет
пользователя в метод действия Index класса MoviesController , который отображает
коллекцию фильмов с учетом только что внесенных изменений.
Перед отправкой формы на сервер на стороне клиента проверяется выполнение
всех правил проверки для полей. При обнаружении ошибок проверки
отображается сообщение об ошибке, а форма не передается. Если JavaScript
отключен, проверка на стороне клиента не выполняется. Тем не менее, сервер
обнаружит переданные недопустимые значения, в результате чего значения
формы будут отображены повторно с сообщениями об ошибках. Далее в этом
руководстве мы более подробно изучим проверку модели. Вспомогательная
функция тега Validation в шаблоне представления Views/Movies/Edit.cshtml
обеспечивает отображение соответствующих сообщений об ошибке.
Все методы HttpGet в контроллере Movie имеют схожий шаблон. Они получают
объект фильма (или список объектов для метода Index ) и передают объект
(модель) в представление. Метод Create передает в представление пустой объект
фильма Create . Все методы, которые создают, редактируют, удаляют или иным
образом изменяют данные, делают это в перегрузке метода [HttpPost] . Изменение
данных в методе HTTP GET сопряжено с угрозой безопасности. Изменение данных в
методе HTTP GET также не соответствует рекомендациям по использованию
протокола HTTP и модели архитектуры REST , в которых указывается, что запросы
GET не должны изменять состояние приложения. Другими словами, операция GET
должна выполняться безопасным способом, то есть не иметь побочных эффектов и
не изменять существующие данные.
Дополнительные ресурсы
Глобализация и локализация
Общие сведения о вспомогательных функциях тегов
Создание вспомогательных функций тегов
Предотвращение атак с межсайтовой подделкой запросов (XSRF/CSRF) в
ASP.NET Core
Защита контроллера от чрезмерной передачи данных
ViewModels
Вспомогательная функция тега Form
Вспомогательная функция тега Input
Вспомогательная функция тега Label
Вспомогательная функция тега Select
Вспомогательная функция тега Validation
Назад
Вперед
Часть 7. Добавление поиска в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 19 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом разделе вы добавите в метод действия Index возможности поиска, которые
позволяют выполнять поиск фильмов по жанру или названию.
Обновите метод Index , находящийся в файле Controllers/MoviesController.cs ,
добавив следующий код:
C#
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Следующая строка в методе Index action создает запрос LINQ для выбора фильмов:
C#
var movies = from m in _context.Movie
select m;
Этот запрос только определяется в этой точке и не выполняется для базы данных.
Если параметр searchString содержит строку, запрос фильмов изменяется для
фильтрации по значению в строке поиска:
C#
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
Приведенный выше код s => s.Title!.Contains(searchString) представляет собой
лямбда-выражение. Лямбда-выражения используются в запросах LINQ на основе
методов в качестве аргументов стандартных методов операторов запроса, таких
как метод Where или Contains (используется в приведенном выше коде). Запросы
LINQ не выполняются, если они определяются или изменяются путем вызова
метода, например Where , Contains или OrderBy . Вместо этого выполнение запроса
откладывается. Это означает, что вычисление выражения откладывается до тех пор,
пока не будет выполнена итерация его реализованного значения или пока не
будет вызван метод ToListAsync . Дополнительные сведения об отложенном и
немедленном выполнении запросов см. в разделе Выполнение запроса.
Примечание. Метод Contains выполняется в базе данных, а не в коде C#,
приведенном выше. Регистр символов запроса учитывается в зависимости от
параметров базы данных и сортировки. В SQL Server метод Contains
сопоставляется с SQL LIKE, в котором регистр символов не учитывается. В SQLite
при параметрах сортировки по умолчанию регистр символов учитывается.
Перейдите к /Movies/Index . Добавьте в URL-адрес строку запроса, например ?
searchString=Ghost . Отображаются отфильтрованные фильмы.
Если вы изменили сигнатуру метода Index и включили в нее параметр с именем
id , параметр id будет соответствовать необязательному заполнителю {id} для
маршрутов по умолчанию, который задан в файле Program.cs .
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Измените параметр на id , а все вхождения searchString — на id .
Предыдущий метод Index :
C#
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Обновленный метод Index с параметром id :
C#
public async Task<IActionResult> Index(string id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie'
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
is null.");
movies = movies.Where(s => s.Title!.Contains(id));
}
return View(await movies.ToListAsync());
}
Теперь можно передать заголовок поиска в качестве данных маршрута (сегмент
URL-адреса) вместо значения строки запроса.
Тем не менее пользователи вряд ли будут каждый раз изменять URL-адрес для
поиска фильмов. Итак, теперь вам необходимо добавить элементы
пользовательского интерфейса для удобства фильтрации фильмов. Если вы
изменили сигнатуру метода Index для тестирования передачи параметра ID с
привязкой к маршруту, измените ее снова, чтобы она снова принимала параметр
searchString :
C#
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Откройте файл Views/Movies/Index.cshtml и добавьте разметку <form> , которая
выделена ниже:
CSHTML
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
Тег HTML <form> использует вспомогательную функцию тега Form, чтобы при
отправке формы строка фильтра передавалась в действие Index контроллера
movies. Сохраните изменения и протестируйте фильтр.
Вопреки ожиданиям, перегрузка [HttpPost] для метода Index отсутствует. Она не
нужна, поскольку метод не изменяет состояние приложения и просто выполняет
фильтрацию данных.
Можно добавить следующий метод [HttpPost] Index .
C#
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
Параметр notUsed используется для создания перегрузки метода Index . Это мы
обсудим далее в этом учебнике.
При добавлении этого метода вызывающий метод действия будет сопоставлять
метод [HttpPost] Index , а метод [HttpPost] Index будет выполняться, как показано
на рисунке ниже.
Тем не менее при добавлении этой версии [HttpPost] метода Index существует
ограничение на общую реализацию. Допустим, вам необходимо добавить в
закладки конкретный поиск или отправить друзьям ссылку, по которой они могут
просмотреть аналогичный отфильтрованный список фильмов. Обратите внимание,
что URL-адрес запроса HTTP POST совпадает с URL-адресом запроса GET (localhost:
{ПОРТ}/Movies/Index) — в URL-адресе отсутствуют сведения о поиске. Данные
строки поиска отправляются на сервер в виде значения поля формы . Вы можете
проверить это с помощью средств разработчика для браузера или инструмента
Fiddler . На рисунке ниже показаны средства разработчика для браузера Chrome:
В теле запроса отображается параметр поиска и маркер XSRF. Обратите внимание,
что, как описывается в предыдущем руководстве, вспомогательная функция тега
Form создает маркер защиты от подделки XSRF. Поскольку мы не изменяем
данные, проверять маркер безопасности в методе контроллера не нужно.
Так как параметр поиска находится в теле запроса, а не в URL-адресе, эти сведения
о поиске нельзя добавить в закладки или открыть для общего доступа. Для
устранения этой проблемы укажите, что запрос типа HTTP GET должен находиться в
файле Views/Movies/Index.cshtml .
CSHTML
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
После отправки поиска URL-адрес содержит строку поискового запроса. Поиск
также переносится в метод HttpGet Index , даже если у вас определен метод
HttpPost Index .
В следующем примере разметки показано изменение тега form :
CSHTML
<form asp-controller="Movies" asp-action="Index" method="get">
Добавление поиска по жанру
Добавьте следующий класс MovieGenreViewModel в папку Models:
C#
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
Модель представления фильмов по жанру будет содержать:
Список фильмов.
Объект SelectList со списком жанров. В этом списке пользователь может
выбрать жанр фильма.
Объект MovieGenre , содержащий выбранный жанр.
SearchString , содержащий текст, который пользователи вводят в поле поиска.
Замените метод Index в файле MoviesController.cs следующим кодом:
C#
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string
searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
Следующий код определяет запрос LINQ , который извлекает все жанры из базы
данных.
C#
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
Объект SelectList со списком жанров создается путем проецирования отдельных
жанров (это необходимо, чтобы исключить повторяющиеся жанры).
Когда пользователь выполняет поиск элемента, значение поиска сохраняется в
поле поиска.
Добавление поиска по жанру в
представление индекса
Обновите файл Index.cshtml , находящийся в папке Views/Movies/ , следующим
образом:
CSHTML
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-routeid="@item.Id">Details</a> |
<a asp-action="Delete" asp-routeid="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Проверьте лямбда-выражение, которое используется в следующем
вспомогательном методе HTML:
@Html.DisplayNameFor(model => model.Movies![0].Title)
В предыдущем коде вспомогательный метод HTML DisplayNameFor проверяет
свойство Title , указанное в лямбда-выражении, и определяет отображаемое имя.
Поскольку лямбда-выражение проверяется, а не вычисляется, в том случае, если
model , model.Movies или model.Movies[0] имеют значение null или пусты, не
происходит нарушение прав доступа. При вычислении лямбда-выражения
(например, @Html.DisplayFor(modelItem => item.Title) ) вычисляются значения для
свойств модели. После ! model.Movies является оператором, допускающим
значение NULL, который используется для объявления , который Movies не имеет
значения NULL.
Проверьте работу приложения, выполнив поиск по жанру, по названию фильма и
по обоим этим параметрам:
Назад
Вперед
Часть 8. Добавление нового поля в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 17 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом разделе Entity Framework Code First Migrations используется для выполнения
следующих задач:
Добавление нового поля в модель.
Перенос нового поля в базу данных.
Если вы используете EF Code First для автоматического создания базы данных, Code
First:
Добавляет таблицу в базу данных для отслеживания схемы базы данных.
Проверяет, синхронизирована ли база данных с классами модели, из которых
она была создана. Если синхронизация нарушена, EF вызывает исключение.
Это позволяет упростить поиск проблем с согласованностью между базой
данных и кодом.
Добавление свойства Rating в модель Movie
Добавьте свойство Rating в Models/Movie.cs :
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
public class Movie
{
public int Id { get; set; }
public string? Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string? Genre { get; set; }
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
public string? Rating {
get; set; }
}
Сборка приложения
Visual Studio
Ctrl+Shift+B
Поскольку в класс Movie было добавлено новое поле, необходимо обновить
список привязки свойств, включив в него новое свойство. В файле
MoviesController.cs обновите атрибут [Bind] для методов действия Create и Edit ,
включив свойство Rating :
C#
[Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]
Обновите шаблоны представлений, чтобы реализовать отображение, создание и
редактирование нового свойства Rating в представлении браузера.
Измените файл /Views/Movies/Index.cshtml и добавьте поле Rating :
CSHTML
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th></th>
</tr>
</thead>
<tbody>
=> model.Movies[0].Title)
=> model.Movies[0].ReleaseDate)
=> model.Movies[0].Genre)
=> model.Movies[0].Price)
=> model.Movies[0].Rating)
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Rating)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-routeid="@item.Id">Details</a> |
<a asp-action="Delete" asp-routeid="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Обновите /Views/Movies/Create.cshtml , добавив поле Rating .
Visual Studio / Visual Studio для Mac
Вы можете скопировать и вставить предыдущую "группу форм" и дождаться
автоматического обновления полей с помощью IntelliSense. IntelliSense
работает со вспомогательными функциями тегов.
Обновите остальные шаблоны.
Обновите класс SeedData так, чтобы он предоставлял значение нового столбца.
Ниже показан пример изменения, которое необходимо выполнить для каждого
new Movie .
C#
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Rating = "R",
Price = 7.99M
},
Для работы приложения необходимо обновить базу данных, включив в нее новое
поле. Если он запущена, возникает следующее исключение SqlException :
SqlException: Invalid column name 'Rating'.
Эта ошибка связана с тем, что обновленный класс модели Movie отличается от
схемы таблицы Movie в существующей базе данных. (В таблице базы данных
отсутствует столбец Rating .)
Устранить эту ошибку можно несколькими способами:
1. Можно с помощью Entity Framework автоматически удалить и повторно
создать базу данных на основе новой схемы класса модели. Этот подход
удобен на ранних стадиях цикла разработки, когда все действия
осуществляются с тестовой базой данных. В этом случае развитие модели и
схемы базы данных осуществляется одновременно. Недостатком такого
подхода является потеря существующих данных в базе, в связи с чем
использовать его в рабочей базе данных невозможно. При разработке
приложения часто используется инициализатор для автоматического
заполнения базы тестовыми данными. Это хороший подход на ранних этапах
разработки и при использовании SQLite.
2. Можно явно изменить схему существующей базы данных в соответствии с
новыми классами модели. Преимущество такого подхода состоит в том, что
сохраняются все данные. Это изменение можно выполнить как вручную, так и
с помощью соответствующего скрипта базы данных.
3. Можно обновить схему базы данных с помощью Code First Migrations.
В этом руководстве используется Code First Migrations.
Visual Studio
В меню Сервис последовательно выберите пункты Диспетчер пакетов NuGet
> Консоль диспетчера пакетов.
В PMC введите следующие команды:
PowerShell
Add-Migration Rating
Update-Database
Команда Add-Migration указывает платформе миграции на необходимость
проверить текущую модель Movie с текущей схемой базы данных Movie и
создать нужный код для переноса базы данных в новую модель.
В качестве имени файла переноса используется произвольное имя "Rating".
Рекомендуется присваивать этому файлу понятное имя.
Если удалить все записи из базы данных, при инициализации она будет
заполнена значениями и в нее будет включено поле Rating .
Запустите приложение и проверьте возможность создания, редактирования и
отображения фильмов с использованием поля Rating .
Назад
Вперед
Часть 9. Добавление проверки в
приложение MVC ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 25 мин
Автор: Рик Андерсон
(Rick Anderson)
Содержание
К модели Movie добавляется логика проверки.
Убедитесь, что правила проверки применяются каждый раз, когда
пользователь создает или редактирует фильм.
Соблюдение принципа "Не повторяйся"
Принцип DRY
(от английского "Don't Repeat Yourself" — не повторяйся) является
одним из основополагающих принципов разработки в модели MVC. В модели
ASP.NET Core MVC рекомендуется задавать функциональные возможности или
поведение только один раз, а затем отражать их в других местах в приложении. Это
позволяет свести к минимуму объем кода, а также снизить риск возникновения в
нем ошибок и упростить его тестирование и поддержку.
Ярким примером применения принципа "Не повторяйся" является поддержка
проверки, реализуемая в модели MVC и на платформе Entity Framework Core Code
First. Правила проверки декларативно определяются в одном месте (в классе
модели) и затем применяются в рамках всего приложения.
Добавление правил проверки к модели
фильма
Пространство имен DataAnnotations предоставляет набор встроенных атрибутов
проверки, которые декларативно применяются к классу или свойству. Кроме того,
DataAnnotations содержит атрибуты форматирования (такие как DataType ), которые
обеспечивают форматирование и не предназначены для проверки.
Обновите класс Movie , чтобы использовать преимущества встроенных атрибутов
проверки Required , StringLength , RegularExpression и Range .
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
public class Movie
{
public int Id { get; set; }
[StringLength(60, MinimumLength = 3)]
[Required]
public string? Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
[Required]
[StringLength(30)]
public string? Genre { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string? Rating { get; set; }
}
Атрибуты проверки определяют поведение для свойств модели, к которым они
применяются:
Атрибуты Required и MinimumLength указывают, что свойство должно иметь
значение. Тем не менее, чтобы удовлетворить требованиям проверки,
пользователю достаточно ввести пробел.
Атрибут RegularExpression ограничивает набор допустимых для ввода
символов. В приведенном выше коде в Genre:
должны использоваться только буквы;
первая буква должна быть прописной; Пробелы разрешены, а цифры и
специальные символы — нет.
В RegularExpression Rating:
первый символ должен быть прописной буквой;
допускаются специальные символы и цифры, а также последующие
пробелы. Значение "PG-13" допустимо для рейтинга, но недопустимо для
жанра.
Атрибут Range ограничивает диапазон значений.
Атрибут StringLength позволяет задать максимальную и при необходимости
минимальную длину строкового свойства.
Типы значений (например, decimal , int , float , DateTime ) по своей природе
являются обязательными и не требуют атрибута [Required] .
Наличие правил проверки, которые автоматически применяются ASP.NET Core,
помогает повысить степень надежности приложения. Это также гарантирует, что в
любом случае будут выполнены все проверки и в базе данных не будут случайно
оставлены поврежденные данные.
Интерфейс ошибки при проверке
Запустите приложение и перейдите к контроллеру фильмов.
Щелкните ссылку Создать, чтобы добавить новый фильм. Введите в форму какиелибо недопустимые значения. Если функция проверки jQuery на стороне клиента
обнаруживает ошибку, сведения о ней отображаются в соответствующем
сообщении.
7 Примечание
Возможно, вы не сможете вводить десятичные запятые в полях для
десятичных чисел. Чтобы обеспечить поддержку проверки jQuery
для
других языков, кроме английского, используйте вместо десятичной точки
запятую (","), а для отображения данных в форматах для других языков, кроме
английского, выполните действия, необходимые для глобализации вашего
приложения. Инструкции по добавлению десятичной запятой см. в этом
комментарии GitHub 4076 .
Обратите внимание, что для каждого поля, содержащего недопустимое значение, в
форме автоматически отображается соответствующее сообщение об ошибке
проверки. Эти ошибки применяются как на стороне клиента (с помощью JavaScript
и jQuery), так и на стороне сервера (если пользователь отключает JavaScript).
Серьезное преимущество заключается в том, что для реализации этого
пользовательского интерфейса проверки не требуется изменять код в классе
MoviesController или представлении Create.cshtml . В контроллере и
представлениях, создаваемых в рамках этого руководства, автоматически
применяются правила проверки, для определения которых к свойствам класса
модели Movie были применены атрибуты. При проверке с использованием метода
действия Edit применяются те же правила.
Данные формы передаются на сервер только после того, как будут устранены все
ошибки проверки на стороне клиента. Чтобы проверить это, установите точку
останова в методе HTTP Post с помощью инструмента Fiddler
или средств
разработчика F12.
Принципы работы проверки
Вам может быть интересно, как пользовательский интерфейс проверки создается
без обновления кода контроллера или представлений. В следующем примере кода
показаны два метода Create .
C#
// GET: Movies/Create
public IActionResult Create()
{
return View();
}
// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you
want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
if (ModelState.IsValid)
{
_context.Add(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(movie);
}
Первый метод действия Create (HTTP GET) отображает исходную форму создания.
Вторая версия ( [HttpPost] ) обрабатывает передачу формы. Второй метод Create
(версия [HttpPost] ) вызывает ModelState.IsValid , который определяет наличие
ошибок проверки в фильме. При вызове этого метода оцениваются все атрибуты
проверки, которые были применены к объекту. При наличии ошибок проверки в
объекте метод Create повторно отображает форму. Если ошибок нет, метод
сохраняет новый фильм в базе данных. В этом примере форма передается на
сервер только после того, как будут устранены все ошибки проверки,
обнаруженные на стороне клиента. Второй метод Create не вызывается до тех пор,
пока на стороне клиента присутствуют ошибки проверки. При отключении
JavaScript в браузере также отключается проверка на стороне клиента. В этом
случае вы можете протестировать метод HTTP POST Create ModelState.IsValid ,
который обнаруживает наличие ошибок проверки.
Вы можете установить точку останова в метод [HttpPost] Create и убедиться, что
он не вызывается и данные формы не передаются, если на стороне клиента
присутствуют ошибки проверки. Если отключить JavaScript в браузере и отправить
форму с ошибками, будет достигнута точка останова. Без JavaScript вы попрежнему будете получать полную проверку.
На следующем рисунке показано, как отключить JavaScript в браузере Firefox.
На следующем рисунке показано, как отключить JavaScript в браузере Chrome.
После отключения JavaScript передайте недопустимые данные и запустите отладку
в пошаговом режиме.
Часть шаблона представления Create.cshtml показана в следующей разметке:
HTML
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
@*Markup removed for brevity.*@
Приведенная выше разметка используется методами действия для отображения
исходной формы и повторного вывода формы в случае ошибки.
Вспомогательная функция тега Input использует атрибуты DataAnnotations и
создает HTML-атрибуты, необходимые для проверки jQuery на стороне клиента.
Вспомогательная функция тега Validation отображает ошибки проверки.
Дополнительные сведения см. в разделе Проверка.
Этот подход удобен тем, что ни контроллер, ни шаблон представления Create
ничего не знают о фактически применяемых правилах проверки или
отображаемых сообщениях об ошибках. Правила проверки и строки ошибок
указываются только в классе Movie . Такие же правила проверки автоматически
применяются к представлению Edit и любым другим представлениям модели,
которые вы можете создавать или редактировать.
При необходимости вы можете изменить логику проверки в одном месте, добавив
атрибуты проверки в модель (в этом примере — в класс Movie ). Вам не придется
беспокоиться о несогласованности применения правил в различных частях
приложения, поскольку вся логика проверки будет определена в одном месте и
начнет применяться по всему приложению. Это позволяет максимально
оптимизировать код и обеспечить удобство его совершенствования и поддержки.
Кроме того, таким образом вы будете полностью соблюдать требования принципа
"Не повторяйся".
Использование атрибутов DataType
Откройте файл Movie.cs и проверьте класс Movie . В пространстве имен
System.ComponentModel.DataAnnotations в дополнение к набору встроенных
атрибутов проверки предоставляются атрибуты форматирования. К полям с датой
выпуска и ценой уже применено значение перечисления DataType . В следующем
коде показаны свойства ReleaseDate и Price с соответствующим атрибутом
DataType .
C#
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
Атрибуты DataType предоставляют модулю просмотра только рекомендации по
форматированию данных, а также другие элементы и атрибуты, например, <a> для
URL-адресов и <a href="mailto:EmailAddress.com"> для электронной почты.
Используйте атрибут RegularExpression для проверки формата данных. Атрибут
DataType позволяет указать тип данных с более точным определением по
сравнению со встроенным типом базы данных, но не предназначен для проверки.
В этом случае требуется отслеживать только дату, но не время. В перечислении
DataType представлено множество типов данных, таких как Date, Time,
PhoneNumber, Currency, EmailAddress и другие. Атрибут DataType также
обеспечивает автоматическое предоставление функций для определенных типов в
приложении. Например, может быть создана ссылка mailto: для
DataType.EmailAddress . Также в браузерах с поддержкой HTML5 может быть
предоставлен селектор даты для DataType.Date . Атрибуты DataType создают
атрибуты HTML 5 data- , которые используются браузерами с поддержкой HTML 5.
Атрибуты DataType не предназначены для проверки.
DataType.Date не задает формат отображаемой даты. По умолчанию поле данных
отображается с использованием форматов, установленных в параметрах
CultureInfo сервера.
С помощью атрибута DisplayFormat можно явно указать формат даты:
C#
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =
true)]
public DateTime ReleaseDate { get; set; }
Параметр ApplyFormatInEditMode указывает, что формат также должен применяться
при отображении значения в текстовом поле для редактирования. (В некоторых
случаях такое поведение нежелательно. Например, в текстовом поле для
редактирования денежных значений обычно не требуется отображать
обозначение денежной единицы.)
Атрибут DisplayFormat может использоваться отдельно, однако чаще всего его
рекомендуется применять вместе с атрибутом DataType . Атрибут DataType передает
семантику данных (в отличие от способа их вывода на экран) и дает следующие
преимущества по сравнению с атрибутом DisplayFormat:
Поддержка функций HTML5 в браузере (отображение элемента управления
календарем, соответствующего языковому стандарту символа валюты, ссылок
электронной почты и т. д.)
По умолчанию формат отображения данных в браузере определяется в
соответствии с установленным языковым стандартом.
С помощью атрибута DataType модель MVC может выбрать подходящий
шаблон поля для отображения данных ( DisplayFormat при отдельном
использовании базируется на строковом шаблоне).
7 Примечание
Проверка jQuery не работает с атрибутом Range и DateTime . Например,
следующий код всегда приводит к возникновению ошибки проверки на
стороне клиента, даже если дата попадает в указанный диапазон:
[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
Чтобы использовать атрибут Range с DateTime , необходимо отключить проверку
дат jQuery. Как правило, не рекомендуется компилировать модели с
фиксированными датами, поэтому использовать атрибуты Range и DateTime следует
крайне осторожно.
В следующем коде демонстрируется объединение атрибутов в одной строке:
C#
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models;
public class Movie
{
public int Id { get; set; }
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }
[Display(Name = "Release Date"), DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
public string Genre { get; set; }
[Range(1, 100), DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}
В следующей части этой серии мы рассмотрим приложение и внесем ряд
изменений в автоматически создаваемые методы Details и Delete .
Дополнительные ресурсы
Работа с формами
Глобализация и локализация
Общие сведения о вспомогательных функциях тегов
Создание вспомогательных функций тегов
Назад
Вперед
Часть 10. Изучение методов Details и
Delete в приложении ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 7 мин
Автор: Рик Андерсон
(Rick Anderson)
Откройте контроллер Movie и изучите метод Details :
C#
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
Подсистема формирования шаблонов MVC, созданная этим методом действия,
добавляет комментарий, показывающий HTTP-запрос, который вызывает метод.
Здесь это запрос GET, состоящий из трех сегментов URL-адреса, контроллера
Movies , метода Details и значения id . Отзыв этих сегментов определяется в
Program.cs .
C#
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
EF упрощает поиск данных с помощью метода FirstOrDefaultAsync . Важной
функцией обеспечения безопасности, встроенной в метод, является то, что код
проверяет, что метод поиска обнаружил фильм до выполнения с ним каких-либо
действий. Например, злоумышленник может внести ошибки на сайт путем
изменения созданного ссылками URL-адреса с http://localhost:
{PORT}/Movies/Details/1 на что-то вроде http://localhost:
{PORT}/Movies/Details/12345 (или любое другое значение, которое не представляет
фактический фильм). Если вы не проверили наличие фильма со значением NULL,
приложение выдаст исключение.
Просмотрите методы Delete и DeleteConfirmed .
C#
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie'
}
is null.");
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
Обратите внимание, что метод HTTP GET Delete не удаляет указанный фильм, он
возвращает представление фильма, где можно выполнить (HttpPost) удаление.
Выполнение операции удаления в ответ на запрос GET (или выполнение операции
редактирования, создания или любой другой операции, изменяющей данные)
открывает брешь в системе безопасности.
Метод [HttpPost] , который удаляет данные, называется DeleteConfirmed , поэтому
метод HTTP POST обладает уникальной сигнатурой или именем. Ниже приведены
сигнатуры двух методов:
C#
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
C#
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
Требуется, чтобы в среде CLR перегруженные методы имели уникальную сигнатуру
параметров (то же имя метода, но другой список параметров). Однако здесь
необходимы два метода Delete — один для GET и один для POST — с одинаковой
сигнатурой параметров. (Они оба должны принимать целочисленное значение в
качестве параметра.)
Существует два подхода к решению этой проблемы, один из которых заключается
в указании разных имен для методов. Именно это было представлено в
предыдущем примере механизма формирования шаблонов. Но в этом случае
возникает небольшая проблема: ASP.NET сопоставляет сегменты URL-адреса с
методами действий по имени, а при переименовании метода, как правило,
маршрутизация не сможет найти этот метод. Решение показано в примере, а
именно: в метод DeleteConfirmed следует добавить атрибут ActionName("Delete") .
Этот атрибут выполняет сопоставление для системы маршрутизации, чтобы URLадрес, включающий /Delete/ для запроса POST, смог найти метод DeleteConfirmed .
Другим распространенным решением проблемы для методов с одинаковыми
именами и сигнатурами является искусственное изменение сигнатуры метода POST
для включения дополнительного (неиспользуемого) параметра. Именно это было
сделано ранее, когда был добавлен параметр notUsed . То же самое можно сделать
для метода [HttpPost] Delete :
C#
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)
Публикация в Azure
Сведения о развертывании в Azure, см. в разделе Учебник: созданию приложения
ASP.NET Core и Базы данных SQL в Службе приложений Azure.
Назад
Учебники по ASP.NET Core Blazor
Статья • 28.01.2023 • Чтение занимает 2 мин
Вот доступные учебники по ASP.NET Core Blazor:
Создание первого приложения Blazor
(Blazor Server)
Создание приложения Blazor со списком дел (Blazor Server или Blazor
WebAssembly)
Использование ASP.NET Core SignalR с Blazor (Blazor Server ил Blazor
WebAssembly)
Учебники по Blazor Hybrid в ASP.NET Core
Модули Learn
Дополнительные сведения о моделях размещения Blazor, Blazor Server и Blazor
WebAssembly, см. в статье Модели размещения Blazor в ASP.NET Core.
Учебник. Создание веб-API с
помощью ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 60 мин
Авторы: Рик Андерсон
(Rick Anderson) и Кирк Ларкин
(Kirk Larkin)
В этом руководстве рассматриваются основы создания веб-API на основе
контроллера, использующего базу данных. Другой подход к созданию API в
ASP.NET Core заключается в создании минимальных API. Сведения о выборе между
минимальными API и API на основе контроллера см. в статье Общие сведения об
API. Руководство по созданию минимального API см. в статье Учебник. Создание
минимального API с помощью ASP.NET Core.
Обзор
В этом руководстве создается следующий API-интерфейс:
API
Описание
Текст
запроса
Текст ответа
GET /api/todoitems
Получение всех элементов
задач
Отсутствуют
Массив элементов
задач
GET
Отсутствуют
Элемент задачи
/api/todoitems/{id}
Получение объекта по
идентификатору
POST /api/todoitems
Добавление нового элемента
Элемент
Элемент задачи
задачи
PUT
Обновление существующего
Элемент
/api/todoitems/{id}
элемента
задачи
DELETE
Удаление элемента
Нет
/api/todoitems/{id}
На следующем рисунке показана структура приложения.
Отсутствуют
Отсутствуют
Предварительные требования
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создайте веб-проект.
Visual Studio
В меню Файл выберите Создать >Проект.
В поле поиска введите Веб-API.
Выберите шаблон Веб-API ASP.NET Core и нажмите кнопку Далее.
В диалоговом окне Настроить новый проект присвойте проекту имя
TodoApi и нажмите кнопку Далее.
В диалоговом окне Дополнительные сведения выполните следующие
действия.
Убедитесь, что платформа является .NET 7.0 (или более поздней
версии).
Убедитесь, что флажок Use controllers (uncheck to use minimal APIs)
(Использовать контроллеры (снимите этот флажок для использования
минимальных API)) установлен.
Нажмите кнопку создания.
7 Примечание
Рекомендации по добавлению пакетов в приложения .NET см. в разделе
Способы установки пакетов NuGet в статье Рабочий процесс использования
пакета (документация по NuGet). Проверьте правильность версий пакета на
сайте NuGet.org .
Тестирование проекта
Шаблон проекта создает API WeatherForecast с поддержкой Swagger.
Visual Studio
Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.
Visual Studio отображает следующее диалоговое окно, если проект еще не
настроен для использования SSL:
Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.
Отобразится следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата
браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio запустит браузер по умолчанию и перейдет к https://localhost:
<port>/swagger/index.html , где <port> — это номер порта, выбранный
случайным образом.
Откроется страница Swagger /swagger/index.html . Выберите Get (Получить)>Try it
out (Попробовать)>Execute (Выполнить). На странице отобразятся:
команда Curl
для тестирования API WeatherForecast;
URL-адрес для тестирования API WeatherForecast;
код, текст и заголовки ответа;
Раскрывающийся список с типами носителей, примером значения и схемы.
Если страница Swagger не отображается, см. эту проблему на сайте GitHub .
Swagger используется для создания полезной документации и страниц справки для
веб-API. В этом учебнике рассматривается создание веб-API. Дополнительные
сведения о Swagger см. в статье Документация по веб-API ASP.NET Core с
использованием Swagger (OpenAPI).
Скопируйте и вставьте URL-адрес запроса в адресную строку браузера:
https://localhost:<port>/weatherforecast
Возвращаемые данные JSON будут примерно такими:
JSON
[
{
"date": "2019-07-16T19:04:05.7257911-06:00",
"temperatureC": 52,
"temperatureF": 125,
"summary": "Mild"
},
{
"date": "2019-07-17T19:04:05.7258461-06:00",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Warm"
},
{
"date": "2019-07-18T19:04:05.7258467-06:00",
"temperatureC": 39,
"temperatureF": 102,
"summary": "Cool"
},
{
"date": "2019-07-19T19:04:05.7258471-06:00",
"temperatureC": 10,
"temperatureF": 49,
"summary": "Bracing"
},
{
"date": "2019-07-20T19:04:05.7258474-06:00",
"temperatureC": -1,
"temperatureF": 31,
"summary": "Chilly"
}
]
Добавление класса модели
Модель — это набор классов, представляющих данные, которыми управляет
приложение. Для этого приложения используется класс модели TodoItem .
Visual Studio
В обозревателе решений щелкните проект правой кнопкой мыши.
Выберите Добавить>Новая папка. Назовите папку Models .
Щелкните папку Models правой кнопкой мыши и выберите пункты
Добавить>Класс. Присвойте классу имя TodoItem и выберите Добавить.
Замените код шаблона следующим кодом:
C#
namespace TodoApi.Models;
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Свойство Id выступает в качестве уникального ключа реляционной базы данных.
Классы моделей можно размещать в любом месте проекта, но обычно для этого
используется папка Models .
Добавление контекста базы данных
Контекст базы данных —это основной класс, который координирует
функциональные возможности Entity Framework для модели данных. Этот класс
является производным от класса Microsoft.EntityFrameworkCore.DbContext.
Visual Studio
Добавление пакетов NuGet
В меню Средства выберите Диспетчер пакетов NuGet > Управление
пакетами NuGet для решения.
Перейдите на вкладку Обзор и введите
Microsoft.EntityFrameworkCore.InMemory в поле поиска.
В области слева щелкните Microsoft.EntityFrameworkCore.InMemory .
Установите флажок Проект в области справа и выберите Установить.
Добавление контекста базы данных
TodoContext
Щелкните папку Models правой кнопкой мыши и выберите пункты
Добавить>Класс. Назовите класс TodoContext и нажмите Добавить.
Введите следующий код:
C#
using Microsoft.EntityFrameworkCore;
namespace TodoApi.Models;
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}
public DbSet<TodoItem> TodoItems { get; set; } = null!;
}
Регистрация контекста базы данных
В ASP.NET Core службы (такие как контекст базы данных) должны быть
зарегистрированы с помощью контейнера внедрения зависимостей. Контейнер
предоставляет службу контроллерам.
Обновите файл Program.cs , используя следующий выделенный код:
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Предыдущий код:
Добавляет using директивы.
Добавляет контекст базы данных в контейнер внедрения зависимостей.
Указывает, что контекст базы данных будет использовать базу данных в
памяти.
Формирование шаблонов контроллера
Visual Studio
Щелкните папку Controllers правой кнопкой мыши.
Щелкните Добавить>Создать шаблонный элемент.
Выберите Контроллер API с действиями, использующий
Entity Framework, а затем выберите Добавить.
В диалоговом окне Контроллер API с действиями, использующий
Entity Framework сделайте следующее:
Выберите TodoItem (TodoApi.Models) в поле Класс модели.
Выберите TodoContext (TodoApi.Models) в поле Класс контекста
данных.
Нажмите Добавить.
Если операция формирования шаблонов завершается неудачно, нажмите
кнопку Добавить, чтобы попытаться сформировать шаблон еще раз.
Сформированный код:
Пометьте этот класс атрибутом [ApiController]. Этот атрибут указывает, что
контроллер отвечает на запросы веб-API. Дополнительные сведения о
поведении, которое реализует этот атрибут, см. в статье Создание веб-API с
помощью ASP.NET Core.
Использует внедрение зависимостей для внедрения контекста базы данных
( TodoContext ) в контроллер. Контекст базы данных используется в каждом
методе создания, чтения, обновления и удаления
в контроллере.
Шаблоны ASP.NET Core для:
Контроллеры с представлениями включают [action] в шаблоне маршрута.
Контроллеры API не включают [action] в шаблоне маршрута.
[action] Если маркер отсутствует в шаблоне маршрута, имя действия (имя метода)
не включается в конечную точку. То есть имя связанного метода действия не
используется в соответствующем маршруте.
Обновление метода создания PostTodoItem
Измените инструкцию возврата в PostTodoItem и используйте оператор nameof:
C#
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
//
return CreatedAtAction("GetTodoItem", new { id = todoItem.Id },
todoItem);
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id },
todoItem);
}
Приведенный выше код является методом HTTP POST , как указано атрибутом
[HttpPost] . Метод получает значение TodoItem из текста HTTP-запроса.
Дополнительные сведения см. в разделе Маршрутизация атрибутов с помощью
атрибутов Http[Verb].
Метод CreatedAtAction:
В случае успеха возвращает код состояния HTTP 201 . HTTP 201 — это
стандартный HTTP POST ответ для метода, который создает новый ресурс на
сервере.
Добавляет в ответ заголовок Location . Заголовок Location указывает URI
новой созданной задачи. Дополнительные сведения см. в статье 10.2.2 201
"Создан ресурс"
.
Указывает действие GetTodoItem для создания URI заголовка Location .
Ключевое слово nameof C# используется для предотвращения жесткого
программирования имени действия в вызове CreatedAtAction .
Тестирование PostTodoItem
Нажмите клавиши CTRL+F5, чтобы запустить приложение.
В окне браузера Swagger выберите POST /api/TodoItems, а затем выберите
Попробовать.
В окне Входные данные текста запроса обновите JSзначение ON. Например,
примененная к объекту директива
JSON
{
"name": "walk dog",
"isComplete": true
}
Нажмите кнопку Выполнить.
Тестирование URI заголовка расположения
В предыдущем запросе POST в пользовательском интерфейсе Swagger
отображается заголовок расположения
в разделе Заголовки ответов. Например,
location: https://localhost:7260/api/TodoItems/1 . В заголовке расположения
отображается универсальный код ресурса (URI) для созданного ресурса.
Чтобы проверить заголовок расположения, выполните следующие действия.
В окне браузера Swagger выберите GET /api/TodoItems/{id}, а затем выберите
Попробовать.
Введите 1 в id поле ввода и нажмите кнопку Выполнить.
Знакомство с методами GET
Реализуются две конечные точки GET:
GET /api/todoitems
GET /api/todoitems/{id}
В предыдущем разделе показан пример /api/todoitems/{id} маршрута.
Следуйте инструкциям POST , чтобы добавить еще один элемент списка дел, а
затем протестировать /api/todoitems маршрут с помощью Swagger.
Это приложение использует выполняющуюся в памяти базу данных. Если
остановить и вновь запустить его, предшествующий запрос GET не возвратит
никаких данных. Если данные не возвращаются, данные для приложения
получаются методом POST.
Маршрутизация и пути URL
Атрибут [HttpGet] обозначает метод, который отвечает на HTTP GET запрос. Путь
URL для каждого метода формируется следующим образом:
Возьмите строку шаблона в атрибуте Route контроллера:
C#
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
Замените [controller] именем контроллера (по соглашению это имя класса
контроллера без суффикса "Controller"). В этом примере класс контроллера
имеет имя TodoItems, а сам контроллер, соответственно, — "TodoItems". В
ASP.NET Core маршрутизация реализуется без учета регистра символов.
Если атрибут [HttpGet] имеет шаблон маршрута (например,
[HttpGet("products")] ), добавьте его к пути. В этом примере шаблон не
используется. Дополнительные сведения см. в разделе Маршрутизация
атрибутов с помощью атрибутов Http[Verb].
В следующем методе GetTodoItem "{id}" — это переменная-заполнитель для
уникального идентификатора элемента задачи. При вызове GetTodoItem параметру
метода id присваивается значение "{id}" в URL-адресе.
C#
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return todoItem;
}
Возвращаемые значения
Тип возвращаемого значения для методов GetTodoItems и GetTodoItem —
ActionResult<T>. ASP.NET Core автоматически сериализует объект в формат JSON
и записывает данные JSON в тело сообщения ответа. Код ответа для этого типа
возвращаемого значения равен 200 OK , что свидетельствует об отсутствии
необработанных исключений. Необработанные исключения преобразуются в
ошибки 5xx.
Типы возвращаемых значений ActionResult могут представлять широкий спектр
кодов состояний HTTP. Например, метод GetTodoItem может возвращать два
разных значения состояния:
Если запрошенному идентификатору не соответствует ни один элемент, метод
возвращает код ошибки 404
NotFound (Не найдено).
В противном случае метод возвращает код 200 с телом ответа в формате
JSON. Возвращает item результат в ответе HTTP 200 .
Метод PutTodoItem
Проверьте метод PutTodoItem .
C#
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
if (id != todoItem.Id)
{
return BadRequest();
}
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
PutTodoItem аналогичен PostTodoItem , за исключением того, что использует . HTTP
PUT Ответ — 204 (Нет содержимого)
. Согласно спецификации HTTP, запрос
требует от PUT клиента отправки всей обновленной сущности, а не только
изменений. Чтобы обеспечить поддержку частичных обновлений, используйте
HTTP PATCH.
Тестирование метода PutTodoItem
В этом примере используется база данных в памяти, которая должна быть
инициирована при каждом запуске приложения. При выполнении вызова PUT в
базе данных уже должен существовать какой-либо элемент. Для этого перед
вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в
базе данных.
С помощью пользовательского интерфейса Swagger нажмите кнопку PUT, чтобы
обновить TodoItem объект с идентификатором = 1 и задать для его имени значение
"feed fish" . Обратите внимание, что ответ имеет значение HTTP 204 No Content
.
Метод DeleteTodoItem
Проверьте метод DeleteTodoItem .
C#
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return NoContent();
}
Тестирование метода DeleteTodoItem
Используйте пользовательский интерфейс Swagger, чтобы удалить объект
TodoItem с идентификатором 1. Обратите внимание, что ответ имеет значение HTTP
204 No Content .
Тестирование с помощью http-repl, Postman
или curl
Для тестирования API часто используются http-repl, Postman
и curl . Swagger
использует curl и отображает отправленную curl команду.
Инструкции по этим средствам см. по следующим ссылкам:
Тестирование API с помощью Postman
Установка и проверка API с помощью http-repl
Дополнительные сведения см. в http-repl статье Тестирование веб-API с помощью
HttpRepl.
Предотвращение избыточной публикации
В настоящее время пример приложения предоставляет весь объект TodoItem .
Рабочие приложения обычно ограничивают вводимые данные и возвращают их с
помощью подмножества модели. Такое поведение реализовано по нескольким
причинам, но в основном из соображений безопасности. Подмножество модели
обычно называется объектом передачи данных (DTO), моделью ввода или
моделью представления. В рамках этого руководства используется DTO.
DTO можно использовать для следующего:
Предотвращение избыточной публикации.
Скрытие свойств, которые не предназначены для просмотра клиентами.
Пропуск некоторых свойств, чтобы уменьшить размер полезной нагрузки.
Сведение графов объектов, содержащих вложенные объекты. Сведенные
графы объектов могут быть удобнее для клиентов.
Чтобы продемонстрировать подход с применением DTO, обновите класс TodoItem ,
включив в него поле секрета:
C#
namespace TodoApi.Models
{
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
}
Поле секрета должно быть скрыто в этом приложении, однако административное
приложение может отобразить его.
Убедитесь, что вы можете отправить и получить секретное поле.
Создайте модель DTO:
C#
namespace TodoApi.Models;
public class TodoItemDTO
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Обновите TodoItemsController для использования TodoItemDTO :
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
namespace TodoApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
private readonly TodoContext _context;
public TodoItemsController(TodoContext context)
{
_context = context;
}
// GET: api/TodoItems
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
{
return await _context.TodoItems
.Select(x => ItemToDTO(x))
.ToListAsync();
}
// GET: api/TodoItems/5
// <snippet_GetByID>
[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
return ItemToDTO(todoItem);
}
// </snippet_GetByID>
// PUT: api/TodoItems/5
// To protect from overposting attacks, see
https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Update>
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO
todoDTO)
{
if (id != todoDTO.Id)
{
return BadRequest();
}
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
todoItem.Name = todoDTO.Name;
todoItem.IsComplete = todoDTO.IsComplete;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
{
return NotFound();
}
return NoContent();
}
// </snippet_Update>
// POST: api/TodoItems
// To protect from overposting attacks, see
https://go.microsoft.com/fwlink/?linkid=2123754
// <snippet_Create>
[HttpPost]
public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO
todoDTO)
{
var todoItem = new TodoItem
{
IsComplete = todoDTO.IsComplete,
Name = todoDTO.Name
};
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetTodoItem),
new { id = todoItem.Id },
ItemToDTO(todoItem));
}
// </snippet_Create>
// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}
_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return NoContent();
}
private bool TodoItemExists(long id)
{
return _context.TodoItems.Any(e => e.Id == id);
}
private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
new TodoItemDTO
{
Id = todoItem.Id,
Name = todoItem.Name,
IsComplete = todoItem.IsComplete
};
}
Убедитесь, что вы можете отправить или получить секретное поле.
Вызов веб-API с помощью JavaScript
См. руководство по : Вызовите веб-API ASP.NET Core с помощью JavaScript.
Серия видео о веб-API
См. видео: Серия для начинающих: Веб-API.
Добавление поддержки аутентификации в
веб-API
ASP.NET Core Identity позволяет использовать функцию входа в пользовательском
интерфейсе для веб-приложений ASP.NET Core. Чтобы защитить веб-API и
одностраничные приложения, используйте один из следующих способов:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
Duende Identity Server
Duende Identity Server — это платформа OpenID Connect и OAuth 2.0 для ASP.NET
Core. Duende Identity Server включает следующие функции безопасности:
Проверка подлинности как услуга (AaaS)
Единый вход (SSO) для нескольких типов приложений
Контроль доступа для API
Шлюз федерации
) Важно!
Компания Duende Software
может потребовать лицензионный сбор за
использование Duende IdentityServer в рабочей среде. Дополнительные
сведения см. в статье Миграция с ASP.NET Core 5.0 на 6.0.
Дополнительные сведения см. в документации по Duende Identity Server (на вебсайте ПО Duende) .
Публикация в Azure
Сведения о развертывании в Azure см. в разделе Краткое руководство.
Развертывание веб-приложения ASP.NET.
Дополнительные ресурсы
Просмотреть или скачать пример кода для этого учебника . См. раздел
Практическое руководство. Скачивание файла.
Дополнительные сведения см. в следующих ресурсах:
Создание веб-API с помощью ASP.NET Core
Руководство. Создание минимального API с помощью ASP.NET Core
Документация по веб-API ASP.NET Core с использованием Swagger (OpenAPI)
Razor Использование Pages с Entity Framework Core в ASP.NET Core:
руководство 1 из 8
Маршрутизация к действиям контроллера в ASP.NET Core
Типы возвращаемых действий контроллера в веб-API ASP.NET Core
Развертывание приложений ASP.NET Core в Службе приложений Azure
Размещение и развертывание ASP.NET Core
Создание веб-API с помощью ASP.NET Core
Руководство. Создание минимального
API с помощью ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 29 мин
Авторы: Рик Андерсон
(Rick Anderson) и Том Дайкстра
(Tom Dykstra)
Архитектура минимальных API позволяет создавать API для HTTP с минимальным
числом зависимостей. Они идеально подходят для микрослужб и приложений,
которым нужен небольшой набор файлов, компонентов и зависимостей на
платформе ASP.NET Core.
В этом руководстве описаны основы создания минимального API с помощью
ASP.NET Core. Другим подходом к созданию API в ASP.NET Core является
использование контроллеров. Сведения о выборе между минимальными API и API
на основе контроллера см. в статье Общие сведения об API. Руководство по
созданию проекта API на основе контроллеров , содержащих дополнительные
функции, см. в статье Создание веб-API.
Обзор
В этом руководстве создается следующий API-интерфейс:
API
Описание
Текст
запроса
Текст ответа
GET /todoitems
Получение всех элементов задач
Отсутствуют
Массив
элементов
задач
GET
Получение элементов выполненных
/todoitems/complete
заданий из списка
GET
Отсутствуют
/todoitems/{id}
Получение объекта по
идентификатору
Элемент
задачи
POST /todoitems
Добавление нового элемента
Элемент
задачи
Элемент
задачи
PUT
Обновление существующего элемента
Элемент
задачи
Отсутствуют
Удаление элемента
Нет
None
/todoitems/{id}
DELETE
/todoitems/{id}
Отсутствуют
Массив
элементов
задач
Предварительные требования
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создание проекта API
Visual Studio
Запустите Visual Studio 2022 и нажмите Создать проект.
В диалоговом окне Создание нового проекта выполните следующие
действия.
Введите Empty в поле Поиск шаблонов.
Выберите шаблон ASP.NET Core Пустой и нажмите кнопку Далее.
Присвойте проекту имя TodoApi и щелкните Далее.
В диалоговом окне Дополнительные сведения выполните следующие
действия.
Выбор .NET 7.0
Снимите флажок Не использовать операторы верхнего уровня
Выберите Создать.
Анализ кода
Файл Program.cs содержит следующий код:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Предыдущий код:
WebApplicationBuilder Создает и WebApplication с предварительно
настроенными значениями по умолчанию.
Создает конечную точку / HTTP GET, которая возвращает Hello World! :
Запуск приложения
Visual Studio
Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.
Visual Studio отображает следующее диалоговое окно.
Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.
Отобразится следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата
браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio запускает Kestrel веб-сервер и открывает окно браузера.
Hello World! отображается в браузере. Файл Program.cs содержит минимальное,
но полное приложение.
Добавление пакетов NuGet
Для поддержки возможностей базы данных и диагностики, которые используются
в этом руководстве, необходимо добавить пакеты NuGet.
Visual Studio
В меню Средства выберите Диспетчер пакетов NuGet > Управление
пакетами NuGet для решения.
Выберите вкладку Обзор.
Введите Microsoft.EntityFrameworkCore.InMemory в поле поиска и
щелкните Microsoft.EntityFrameworkCore.InMemory .
Установите флажок Проект в области справа и выберите Установить.
Добавьте пакет Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore по
представленным выше инструкциям.
Классы контекста базы данных и модели
В папке проекта создайте файл Todo.cs со следующим кодом:
C#
class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
Приведенный выше код создает модель для этого приложения. Класс модели
представляет данные, которыми управляет наше приложение.
Создайте файл с именем TodoDb.cs со следующим кодом:
C#
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Приведенный выше код определяет контекст базы данных, который является
основным классом, который координирует функциональные возможности Entity
Framework для модели данных. Этот класс является производным от класса
Microsoft.EntityFrameworkCore.DbContext.
Добавление кода API
Замените содержимое файла Program.cs приведенным ниже кодом.
C#
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
app.Run();
Выделенный ниже код добавляет контекст базы данных в контейнер внедрения
зависимостей (DI) и позволяет отображать исключения, связанные с базой данных:
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
Контейнер DI предоставляет доступ к контексту базы данных и другим службам.
Установка Postman для тестирования
приложения
В этом учебнике для тестирования API используется Postman.
Установка Postman
Запустите веб-приложение.
Запустите Postman.
Отключение параметра Проверка SSL-сертификата
Для Postman для Windows выберитеПараметрыфайла> (вкладка Общие) и
отключите проверку SSL-сертификата.
Для Postman для macOS выберитеПараметрыPostman> (вкладка Общие) и
отключите проверку SSL-сертификата.
2 Предупреждение
Повторно включите проверку SSL-сертификата после тестирования
примера приложения.
Проверка публикации данных
Следующий код в создает Program.cs конечную точку /todoitems HTTP POST,
которая добавляет данные в базу данных в памяти:
C#
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Запустите приложение. В браузере отображается ошибка 404, так как конечная /
точка больше не существует.
Используйте конечную точку POST для добавления данных в приложение:
Создайте новый HTTP-запрос.
Установите HTTP-метод POST .
Задайте для URI значение https://localhost:<port>/todoitems . Пример:
https://localhost:5001/todoitems
Откройте вкладку Тело.
Выберите необработанный.
Выберите тип JSON.
В теле запроса введите код JSON для элемента списка дел:
JSON
{
"name":"walk dog",
"isComplete":true
}
Нажмите кнопку Отправить.
Изучение конечных точек GET
Пример приложения реализует несколько конечных точек GET путем вызова
MapGet :
API
Описание
Текст
запроса
Текст ответа
GET /todoitems
Получение всех элементов задач
Отсутствуют
Массив
элементов задач
GET
Получение всех выполненных
Отсутствуют
Массив
/todoitems/complete
элементов заданий
GET
Получение объекта по
идентификатору
/todoitems/{id}
C#
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
элементов задач
Отсутствуют
Элемент задачи
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Тестирование конечных точек GET
Протестируйте приложение, вызвав конечные точки из браузера или Postman.
Следующие действия предназначены для Postman.
Создайте новый HTTP-запрос.
Укажите метод HTTP GET.
Задайте для URI запроса значение https://localhost:<port>/todoitems .
Например, https://localhost:5001/todoitems .
Нажмите кнопку Отправить.
Вызов метода GET /todoitems создает ответ, аналогичный следующему:
JSON
[
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
]
Задайте для URI запроса значение https://localhost:<port>/todoitems/1 .
Например, https://localhost:5001/todoitems/1 .
Нажмите кнопку Отправить.
Ответ аналогичен следующему:
JSON
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
Это приложение использует выполняющуюся в памяти базу данных. После
перезапуска приложения запрос GET не возвращает никаких данных. Если данные
не возвращаются, отправьте в приложение данные POST и повторите запрос GET.
Возвращаемые значения
ASP.NET Core автоматически сериализует объект в формат JSON
и записывает
данные JSON в тело сообщения ответа. Код ответа для этого типа возвращаемого
значения равен 200 OK , что свидетельствует об отсутствии необработанных
исключений. Необработанные исключения преобразуются в ошибки 5xx.
Типы возвращаемых значений могут представлять широкий спектр кодов
состояний HTTP. Например, метод GET /todoitems/{id} может возвращать два
разных значения состояния:
Если запрошенному идентификатору не соответствует ни один элемент, метод
возвращает код ошибки 404
NotFound (Не найдено).
В противном случае метод возвращает код 200 с телом ответа в формате
JSON. При возвращении item возвращается ответ HTTP 200.
Изучение конечной точки PUT
Этот пример приложения реализует одну конечную точку PUT с помощью MapPut :
C#
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
Этот метод отличается от метода MapPost только тем, что использует метод HTTP
PUT. Успешный ответ возвращает состояние 204 (без содержимого) . Согласно
спецификации HTTP, запрос PUT требует, чтобы клиент отправлял всю
обновленную сущность, а не только изменения. Чтобы обеспечить поддержку
частичных обновлений, используйте HTTP PATCH.
Тестирование конечной точки PUT
В этом примере используется база данных в памяти, которая должна быть
инициирована при каждом запуске приложения. При выполнении вызова PUT в
базе данных уже должен существовать какой-либо элемент. Для этого перед
вызовом PUT выполните вызов GET, чтобы убедиться в наличии такого элемента в
базе данных.
Обновите элемент списка дел с идентификатором 1 и присвойте ему имя "feed
fish" :
JSON
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
Изучение и тестирование конечной точки
DELETE
Этот пример приложения реализует одну конечную точку DELETE с помощью
MapDelete :
C#
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
Удалите элемент списка дел с помощью Postman:
Укажите метод DELETE .
Укажите URI удаляемого объекта (например,
https://localhost:5001/todoitems/1 ).
Нажмите кнопку Отправить.
Использование API MapGroup
Пример кода приложения повторяет todoitems префикс URL-адреса при каждой
настройке конечной точки. API-интерфейсы часто имеют группы конечных точек с
общим префиксом URL-адресов, и MapGroup для организации таких групп
доступен метод . Он сокращает количество повторяющихся кодов и позволяет
настраивать целые группы конечных точек с помощью одного вызова таких
методов, как RequireAuthorization и WithMetadata.
Замените все содержимое Program.cs следующим кодом:
C#
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
app.Run();
Приведенный выше код содержит следующие изменения:
Добавляет var todoItems = app.MapGroup("/todoitems"); для настройки группы
с помощью префикса /todoitems URL-адреса .
Изменяет все app.Map<HttpVerb> методы на todoItems.Map<HttpVerb> .
Удаляет префикс /todoitems URL-адреса из Map<HttpVerb> вызовов метода.
Протестируйте конечные точки, чтобы убедиться, что они работают одинаково.
Использование API TypedResults
Методы Map<HttpVerb> могут вызывать методы обработчика маршрутов вместо
использования лямбда-выражений. Чтобы просмотреть пример, обновите Файл
Program.cs , используя следующий код:
C#
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t =>
t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.Ok(todo);
}
return TypedResults.NotFound();
}
Теперь Map<HttpVerb> код вызывает методы вместо лямбда-выражений:
C#
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
Эти методы возвращают объекты, которые реализуют IResult и определяются с
помощью TypedResults:
C#
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t =>
t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.Ok(todo);
}
return TypedResults.NotFound();
}
Модульные тесты могут вызывать эти методы и проверять, что они возвращают
правильный тип. Например, если метод имеет значение GetAllTodos :
C#
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Код модульного теста может проверить, возвращается ли объект типа Ok<Todo[]>
из метода обработчика. Например:
C#
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Предотвращение избыточной публикации
В настоящее время пример приложения предоставляет весь объект Todo . Рабочие
приложения обычно ограничивают вводимые данные и возвращают их с
помощью подмножества модели. Это связано с несколькими причинами, и
безопасность является основной. Подмножество модели обычно называется
объектом передачи данных (DTO), моделью ввода или моделью представления. В
этой статье используется DTO.
DTO можно использовать для следующего:
Предотвращение избыточной публикации.
Скрытие свойств, которые не предназначены для просмотра клиентами.
Пропуск некоторых свойств, чтобы уменьшить размер полезной нагрузки.
Сведение графов объектов, содержащих вложенные объекты. Сведенные
графы объектов могут быть удобнее для клиентов.
Чтобы продемонстрировать подход с применением DTO, обновите класс Todo ,
включив в него поле секрета:
C#
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
Поле секрета должно быть скрыто в этом приложении, однако административное
приложение может отобразить его.
Убедитесь, что вы можете отправить и получить секретное поле.
Создайте файл с именем TodoItemDTO.cs со следующим кодом:
C#
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name,
todoItem.IsComplete);
}
Обновите код в , Program.cs чтобы использовать эту модель DTO:
C#
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt =>
opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new
TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO,
TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.Ok(todo);
}
return TypedResults.NotFound();
}
Убедитесь, что вы можете опубликовать и получить все поля, кроме поля секрета.
Дальнейшие действия
Настройка параметров сериализации JSON
Сведения о настройке JSсериализации ON в приложениях Минимального API см. в
разделе Настройка JSпараметров сериализации ON.
Обработка ошибок и исключений
Страница исключений разработчика включена по умолчанию в среде разработки
для минимальных приложений API. Сведения об обработке ошибок и исключений
см. в статье Обработка ошибок в API ASP.NET Core.
Тестирование минимальных приложений API
Пример тестирования для минимального приложения API см. в этом примере на
GitHub .
Использование OpenAPI (Swagger)
Сведения об использовании OpenAPI с минимальными приложениями API см. в
статье Поддержка OpenAPI в минимальных API.
Публикация в Azure
Сведения о развертывании в Azure см. в статье Краткое руководство.
Развертывание веб-приложения ASP.NET.
Подробнее
Дополнительные сведения о минимальных приложениях API см. в кратком
справочнике по минимальным API.
Создание веб-API с помощью ASP.NET
Core и MongoDB
Статья • 28.01.2023 • Чтение занимает 19 мин
Авторы Пратик Ханделвал
(Pratik Khandelwal) и Скотт Эдди
(Scott Addie).
В этом руководстве описано, как создать веб-API, который выполняет операции
создания, чтения, обновления и удаления (CRUD) с базой данных NoSQL
MongoDB .
В этом руководстве вы узнаете, как:
" Настройка MongoDB
" создать базу данных MongoDB;
" определить коллекцию и схему MongoDB;
" выполнить операции CRUD MongoDB из веб-API.
" Настройка сериализации JSON
Предварительные требования
MongoDB
оболочка MongoDB ,
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Настройка MongoDB
В Windows MongoDB по умолчанию устанавливается в папку C:\Program
Files\MongoDB. Добавьте C:\Program Files\MongoDB\Server\version_number>\bin в
переменную среды Path . Это изменение обеспечит доступ к MongoDB из любого
места на компьютере разработки.
В следующих шагах используйте ранее установленный интерфейс MongoDB, чтобы
создать базу данных и коллекции и сохранить документы. Дополнительные
сведения о командах MongoDB: mongosh .
1. Выберите папку на компьютере разработки для хранения данных. Например
C:\BooksData при работе в Windows. Если такого каталога нет, создайте его. В
mongo Shell нельзя создавать каталоги.
2. Откройте командную оболочку. Выполните следующую команду для
подключения к MongoDB через порт 27017, заданный по умолчанию. Не
забудьте заменить <data_directory_path> каталогом, созданным на
предыдущем этапе.
Консоль
mongod --dbpath <data_directory_path>
3. Откройте другой экземпляр командной оболочки. Подключитесь к тестовой
базе данных по умолчанию, выполнив такую команду:
Консоль
mongosh
4. В командной оболочке выполните следующую команду:
Консоль
use BookStore
Будет создана база данных с именем BookStore, если она не существует. Если
такая база данных существует, для нее уже установлено подключение для
транзакций.
5. Создайте коллекцию Books с помощью такой команды:
Консоль
db.createCollection('Books')
Отобразится такой результат:
Консоль
{ "ok" : 1 }
6. Определите схему для коллекции Books и вставьте два документа, используя
следующую команду:
Консоль
db.Books.insertMany([{ "Name": "Design Patterns", "Price": 54.93,
"Category": "Computers", "Author": "Ralph Johnson" }, { "Name": "Clean
Code", "Price": 43.15, "Category": "Computers","Author": "Robert C.
Martin" }])
Отобразится примерно такой результат:
Консоль
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("61a6058e6c43f32854e51f51"),
ObjectId("61a6058e6c43f32854e51f52")
]
}
7 Примечание
ObjectId , показанные в предыдущем результате, не будут соответствовать
отображаемому в вашей командной оболочке.
7. Просмотрите документы в базе данных, используя такую команду:
Консоль
db.Books.find().pretty()
Отобразится примерно такой результат:
Консоль
{
"_id" : ObjectId("61a6058e6c43f32854e51f51"),
"Name" : "Design Patterns",
"Price" : 54.93,
"Category" : "Computers",
"Author" : "Ralph Johnson"
}
{
"_id" : ObjectId("61a6058e6c43f32854e51f52"),
"Name" : "Clean Code",
"Price" : 43.15,
"Category" : "Computers",
"Author" : "Robert C. Martin"
}
Схема добавляет автоматически созданное свойство _id типа ObjectId к
каждому документу.
Создание проекта веб-API ASP.NET Core
Visual Studio
1. Выберите ФайлСоздатьПроект.
2. Выберите тип проекта Веб-API ASP.NET Core и щелкните Далее.
3. Назовите проект BookStoreApi и щелкните Создать.
4. Выберите платформу .NET 6.0 (долгосрочная поддержка) и щелкните
Создать.
5. В окне Консоль диспетчера пакетов перейдите в корневую папку
проекта. Выполните следующую команду, чтобы установить драйвер .NET
для MongoDB:
PowerShell
Install-Package MongoDB.Driver
Добавление модели сущности
1. Добавьте каталог Models в корневую папку проекта.
2. Добавьте класс Book в каталог Book с помощью такого кода:
C#
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace BookStoreApi.Models;
public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
[BsonElement("Name")]
public string BookName { get; set; } = null!;
public decimal Price { get; set; }
public string Category { get; set; } = null!;
public string Author { get; set; } = null!;
}
В классе выше свойство Id :
Требуется для сопоставления объекта среды CLR с коллекцией MongoDB.
Помечается с помощью [BsonId]
для назначения этого свойства в
качестве первичного ключа документа.
Помечается с помощью [BsonRepresentation(BsonType.ObjectId)] , чтобы
разрешить передачу параметра в качестве типа string вместо структуры
[BsonRepresentation(BsonType.ObjectId)] . Mongo обрабатывает
преобразование из string в ObjectId .
Свойство BookName помечено атрибутом [BsonElement] . Значение атрибута
Name представляет имя свойства в коллекции MongoDB.
Добавление модели конфигурации
1. Добавьте в файл appsettings.json следующие значения конфигурации базы
данных:
JSON
{
"BookStoreDatabase": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookStore",
"BooksCollectionName": "Books"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
2. Добавьте класс BookStoreDatabaseSettings в каталог BookStoreDatabaseSettings
с помощью такого кода:
C#
namespace BookStoreApi.Models;
public class BookStoreDatabaseSettings
{
public string ConnectionString { get; set; } = null!;
public string DatabaseName { get; set; } = null!;
public string BooksCollectionName { get; set; } = null!;
}
Предыдущий класс BookStoreDatabaseSettings используется для хранения
значений свойств BookStoreDatabase файла appsettings.json . Свойства JSON и
C# имеют одинаковые имена, что упрощает сопоставление.
3. Добавьте выделенный ниже код в Program.cs :
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));
В предыдущем коде экземпляр конфигурации, к которому привязан раздел
BookStoreDatabase файла appsettings.json , зарегистрирован в контейнере
внедрения зависимостей (DI). Например, свойство ConnectionString объекта
BookStoreDatabaseSettings заполняется свойством
BookStoreDatabase:ConnectionString в appsettings.json .
4. Добавьте следующий код в самое начало файла Program.cs , чтобы разрешить
ссылку BookStoreDatabaseSettings :
C#
using BookStoreApi.Models;
Добавление службы операций CRUD
1. Добавьте каталог Services в корневую папку проекта.
2. Добавьте класс BooksService в каталог BooksService с помощью такого кода:
C#
using BookStoreApi.Models;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
namespace BookStoreApi.Services;
public class BooksService
{
private readonly IMongoCollection<Book> _booksCollection;
public BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
bookStoreDatabaseSettings.Value.DatabaseName);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
public async Task<List<Book>> GetAsync() =>
await _booksCollection.Find(_ => true).ToListAsync();
public async Task<Book?> GetAsync(string id) =>
await _booksCollection.Find(x => x.Id ==
id).FirstOrDefaultAsync();
public async Task CreateAsync(Book newBook) =>
await _booksCollection.InsertOneAsync(newBook);
public async Task UpdateAsync(string id, Book updatedBook) =>
await _booksCollection.ReplaceOneAsync(x => x.Id == id,
updatedBook);
public async Task RemoveAsync(string id) =>
await _booksCollection.DeleteOneAsync(x => x.Id == id);
}
В предыдущем коде экземпляр BookStoreDatabaseSettings извлекается из DI
путем внедрения конструктора. Таким образом обеспечивается доступ к
значениям конфигурации в файле appsettings.json , которые были добавлены
в разделе appsettings.json .
3. Добавьте выделенный ниже код в Program.cs :
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));
builder.Services.AddSingleton<BooksService>();
В предыдущем коде класс BooksService регистрируется в DI, чтобы обеспечить
поддержку внедрения через конструктор в используемые классы. Время
существования отдельной службы — наиболее подходящий вариант, так как
BooksService имеет прямую зависимость от MongoClient . В соответствии с
официальными правилами повторного использования клиента Mongo
следует регистрировать в DI с использованием времени существования
отдельной службы.
4. Добавьте следующий код в самое начало файла Program.cs , чтобы разрешить
ссылку BooksService :
C#
using BookStoreApi.Services;
Класс BooksService использует следующие члены MongoDB.Driver для выполнения
операций CRUD с базой данных:
MongoClient
— считывает экземпляр сервера для выполнения операций с
базой данных. Конструктор этого класса предоставляет строку подключения
MongoDB.
C#
public BooksService(
IOptions<BookStoreDatabaseSettings> bookStoreDatabaseSettings)
{
var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(
bookStoreDatabaseSettings.Value.DatabaseName);
_booksCollection = mongoDatabase.GetCollection<Book>(
bookStoreDatabaseSettings.Value.BooksCollectionName);
}
IMongoDatabase
— представляет базу данных Mongo для выполнения
операций. В этом руководстве используется универсальный метод
GetCollection
TDocument>(collection) интерфейса для получения доступа к
данным в определенной коллекции. Выполняйте операции CRUD с
коллекцией после вызова этого метода. В вызове метода
GetCollection<TDocument>(collection) :
collection представляет имя коллекции;
TDocument представляет тип объекта среды CLR, хранящегося в коллекции;
GetCollection<TDocument>(collection) возвращает объект GetCollection<TDocument>
(collection) , представляющий коллекцию. В этом руководстве следующие методы
вызываются для коллекции:
DeleteOne
— удаляет один документ, отвечающий заданным критериям
поиска.
Find
TDocument> — возвращает все документы в коллекции,
соответствующие заданным критериям поиска.
InsertOneAsync
— вставляет предоставленный объект в виде нового
документа в коллекции.
ReplaceOneAsync
— заменяет один документ, отвечающий заданным
критериям поиска, предоставленным объектом.
Добавление контроллера
Добавьте класс BooksController в каталог BooksController с помощью такого кода:
C#
using BookStoreApi.Models;
using BookStoreApi.Services;
using Microsoft.AspNetCore.Mvc;
namespace BookStoreApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BooksService _booksService;
public BooksController(BooksService booksService) =>
_booksService = booksService;
[HttpGet]
public async Task<List<Book>> Get() =>
await _booksService.GetAsync();
[HttpGet("{id:length(24)}")]
public async Task<ActionResult<Book>> Get(string id)
{
var book = await _booksService.GetAsync(id);
if (book is null)
{
return NotFound();
}
return book;
}
[HttpPost]
public async Task<IActionResult> Post(Book newBook)
{
await _booksService.CreateAsync(newBook);
return CreatedAtAction(nameof(Get), new { id = newBook.Id },
newBook);
}
[HttpPut("{id:length(24)}")]
public async Task<IActionResult> Update(string id, Book updatedBook)
{
var book = await _booksService.GetAsync(id);
if (book is null)
{
return NotFound();
}
updatedBook.Id = book.Id;
await _booksService.UpdateAsync(id, updatedBook);
return NoContent();
}
[HttpDelete("{id:length(24)}")]
public async Task<IActionResult> Delete(string id)
{
var book = await _booksService.GetAsync(id);
if (book is null)
{
return NotFound();
}
await _booksService.RemoveAsync(id);
return NoContent();
}
}
Предыдущий контроллер веб-API:
использует класс BooksService для выполнения операций CRUD;
содержит методы действий для поддержки запросов HTTP GET, POST, PUT и
DELETE.
Вызывает CreatedAtAction в методе действия Create для возврата ответа
CreatedAtAction. Код состояния 201 представляет собой стандартный ответ
метода HTTP POST, создающего ресурс на сервере. CreatedAtAction также
добавляет заголовок Location в ответ. Заголовок Location указывает
универсальный код ресурса (URI) созданной книги.
Тестирование веб-API
1. Выполните сборку и запуск приложения.
2. Перейдите к https://localhost:<port>/api/books , где <port> — это
автоматически назначаемый номер порта для приложения, чтобы
протестировать метод действия Get контроллера без параметров.
Отобразится примерно такой ответ JSON:
JSON
[
{
"id": "61a6058e6c43f32854e51f51",
"bookName": "Design Patterns",
"price": 54.93,
"category": "Computers",
"author": "Ralph Johnson"
},
{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}
]
3. Перейдите по адресу https://localhost:<port>/api/books/{id here} , чтобы
протестировать перегруженный метод действия Get контроллера.
Отобразится примерно такой ответ JSON:
JSON
{
"id": "61a6058e6c43f32854e51f52",
"bookName": "Clean Code",
"price": 43.15,
"category": "Computers",
"author": "Robert C. Martin"
}
Настройка параметров сериализации JSON
Нужно изменить два параметра для возвращаемых ответов JSON в разделе
Тестирование веб-API:
Смешанный регистр имен свойств по умолчанию следует изменить в
соответствии с регистром Pascal имен свойств объекта CLR.
Свойство bookName должно возвращаться как Name .
Чтобы удовлетворить эти требования, внесите следующие изменения:
1. В Program.cs вставьте следующий выделенный код в вызов метода
AddControllers :
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.Configure<BookStoreDatabaseSettings>(
builder.Configuration.GetSection("BookStoreDatabase"));
builder.Services.AddSingleton<BooksService>();
builder.Services.AddControllers()
.AddJsonOptions(
options => options.JsonSerializerOptions.PropertyNamingPolicy =
null);
После предыдущего изменения имена свойств в ответе сериализованного
JSON веб-API соответствуют именам свойств в типе объекта CLR. Например,
свойство Author класса Book сериализуется как Author , а не author .
2. В Models/Book.cs добавьте к свойству BookName атрибут [JsonPropertyName]:
C#
[BsonElement("Name")]
[JsonPropertyName("Name")]
public string BookName { get; set; } = null!;
Атрибут [JsonPropertyName] со значением Name представляет имя свойства в
сериализованном ответе веб-API в формате JSON.
3. Добавьте следующий код в самое начало файла Models/Book.cs , чтобы
разрешить ссылку на атрибут [JsonProperty] :
C#
using System.Text.Json.Serialization;
4. Повторите действия, описанные в разделе Тестирование веб-API. Обратите
внимание на различие в именах свойств JSON.
Добавление поддержки аутентификации в
веб-API
ASP.NET Core Identity позволяет использовать функцию входа в пользовательском
интерфейсе для веб-приложений ASP.NET Core. Чтобы защитить веб-API и
одностраничные приложения, используйте один из следующих способов:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
Duende Identity Server
Duende Identity Server — это платформа OpenID Connect и OAuth 2.0 для ASP.NET
Core. Duende Identity Server включает следующие функции безопасности:
Проверка подлинности как услуга (AaaS)
Единый вход (SSO) для нескольких типов приложений
Контроль доступа для API
Шлюз федерации
) Важно!
Компания Duende Software
может потребовать лицензионный сбор за
использование Duende IdentityServer в рабочей среде. Дополнительные
сведения см. в статье Миграция с ASP.NET Core 5.0 на 6.0.
Дополнительные сведения см. в документации по Duende Identity Server (на вебсайте ПО Duende) .
Дополнительные ресурсы
Просмотреть или скачать образец кода
(как скачивать)
Создание веб-API с помощью ASP.NET Core
Типы возвращаемых действий контроллера в веб-API ASP.NET Core
Создание веб-API с помощью ASP.NET Core
Учебник. Вызов веб-API ASP.NET Core
с помощью JavaScript
Статья • 28.01.2023 • Чтение занимает 10 мин
Автор: Рик Андерсон
(Rick Anderson)
В этом руководстве описано, как вызвать веб-API ASP.NET Core с помощью
JavaScript и Fetch API
.
Предварительные требования
Изучите Учебник. Создание веб-API.
Опыт работы с CSS, HTML и JavaScript.
Вызов веб-API с помощью JavaScript
В этом разделе описано, как добавить HTML-страницу, содержащую формы для
создания и администрирования элементов списка задач. Обработчики событий
присоединяются к элементам на странице. При использовании обработчиков
событий создаются запросы HTTP к методам действия веб-API. Функция Fetch API
fetch инициирует каждый такой запрос HTTP.
Функция fetch возвращает объект Promise , который содержит ответ HTTP,
представленный в виде объекта Response . Распространенным подходом является
извлечение текста ответа JSON путем вызова функции json для объекта Response .
JavaScript изменяет страницу, используя сведения из ответа API.
Самый простой вызов fetch принимает один параметр, представляющий маршрут.
Второй параметр (объект init ) является необязательным. init используется для
настройки запроса HTTP.
1. Настройте в приложении обслуживание статических файлов и включите
сопоставление файлов по умолчанию. Вставьте в Program.cs следующий
выделенный код:
C#
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
1. Создайте папку wwwroot в корневом каталоге проекта.
2. Создайте папку css в папке wwwroot.
3. Создайте папку js в папке wwwroot.
4. Добавьте HTML-файл index.html в папку wwwroot. Замените содержимое
файла index.html следующей разметкой:
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>To-do CRUD</title>
<link rel="stylesheet" href="css/site.css" />
</head>
<body>
<h1>To-do CRUD</h1>
<h3>Add</h3>
<form action="javascript:void(0);" method="POST"
onsubmit="addItem()">
<input type="text" id="add-name" placeholder="New to-do">
<input type="submit" value="Add">
</form>
<div id="editForm">
<h3>Edit</h3>
<form action="javascript:void(0);" onsubmit="updateItem()">
<input type="hidden" id="edit-id">
<input type="checkbox" id="edit-isComplete">
<input type="text" id="edit-name">
<input type="submit" value="Save">
<a onclick="closeInput()" aria-label="Close">✖</a>
</form>
</div>
<p id="counter"></p>
<table>
<tr>
<th>Is Complete?</th>
<th>Name</th>
<th></th>
<th></th>
</tr>
<tbody id="todos"></tbody>
</table>
<script src="js/site.js" asp-append-version="true"></script>
<script type="text/javascript">
getItems();
</script>
</body>
</html>
5. Добавьте CSS-файл с именем site.css в папку wwwroot/css. Замените
содержимое файла site.css следующими стилями:
css
input[type='submit'], button, [aria-label] {
cursor: pointer;
}
#editForm {
display: none;
}
table {
font-family: Arial, sans-serif;
border: 1px solid;
border-collapse: collapse;
}
th {
background-color: #f8f8f8;
padding: 5px;
}
td {
border: 1px solid;
padding: 5px;
}
6. Добавьте файл JavaScript с именем site.js в папку wwwroot/js. Замените все
содержимое site.js следующим кодом:
JavaScript
const uri = 'api/todoitems';
let todos = [];
function getItems() {
fetch(uri)
.then(response => response.json())
.then(data => _displayItems(data))
.catch(error => console.error('Unable to get items.', error));
}
function addItem() {
const addNameTextbox = document.getElementById('add-name');
const item = {
isComplete: false,
name: addNameTextbox.value.trim()
};
fetch(uri, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(response => response.json())
.then(() => {
getItems();
addNameTextbox.value = '';
})
.catch(error => console.error('Unable to add item.', error));
}
function deleteItem(id) {
fetch(`${uri}/${id}`, {
method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
}
function displayEditForm(id) {
const item = todos.find(item => item.id === id);
document.getElementById('edit-name').value = item.name;
document.getElementById('edit-id').value = item.id;
document.getElementById('edit-isComplete').checked = item.isComplete;
document.getElementById('editForm').style.display = 'block';
}
function updateItem() {
const itemId = document.getElementById('edit-id').value;
const item = {
id: parseInt(itemId, 10),
isComplete: document.getElementById('edit-isComplete').checked,
name: document.getElementById('edit-name').value.trim()
};
fetch(`${uri}/${itemId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));
closeInput();
return false;
}
function closeInput() {
document.getElementById('editForm').style.display = 'none';
}
function _displayCount(itemCount) {
const name = (itemCount === 1) ? 'to-do' : 'to-dos';
document.getElementById('counter').innerText = `${itemCount}
${name}`;
}
function _displayItems(data) {
const tBody = document.getElementById('todos');
tBody.innerHTML = '';
_displayCount(data.length);
const button = document.createElement('button');
data.forEach(item => {
let isCompleteCheckbox = document.createElement('input');
isCompleteCheckbox.type = 'checkbox';
isCompleteCheckbox.disabled = true;
isCompleteCheckbox.checked = item.isComplete;
let editButton = button.cloneNode(false);
editButton.innerText = 'Edit';
editButton.setAttribute('onclick', `displayEditForm(${item.id})`);
let deleteButton = button.cloneNode(false);
deleteButton.innerText = 'Delete';
deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);
let tr = tBody.insertRow();
let td1 = tr.insertCell(0);
td1.appendChild(isCompleteCheckbox);
let td2 = tr.insertCell(1);
let textNode = document.createTextNode(item.name);
td2.appendChild(textNode);
let td3 = tr.insertCell(2);
td3.appendChild(editButton);
let td4 = tr.insertCell(3);
td4.appendChild(deleteButton);
});
todos = data;
}
Может потребоваться изменение параметров запуска проекта ASP.NET Core для
локального тестирования HTML-страницы:
1. Откройте файл Properties\launchSettings.json.
2. Удалите свойство launchUrl , чтобы приложение открылось через index.html
— файл проекта по умолчанию.
В этом примере вызываются все методы CRUD в веб-API. Ниже приводится
пояснение запросов веб-API.
Получение списка элементов задач
В следующем коде HTTP-запрос GET направляется по пути api/todoitems:
JavaScript
fetch(uri)
.then(response => response.json())
.then(data => _displayItems(data))
.catch(error => console.error('Unable to get items.', error));
Когда веб-API возвращает код состояния, указывающий на успешное выполнение,
вызывается функция _displayItems . Каждый элемент списка задач в параметре
массива, который принимается _displayItems , добавляется в таблицу с помощью
кнопок Изменить и Удалить. Если запрос веб-API завершается сбоем, в консоли
браузера регистрируется сообщение об ошибке.
Добавление элемента задачи
В приведенном ниже коде выполняется следующее:
Переменная item объявляется для создания представления объектного
литерала элемента списка задач.
Для запроса Fetch настраиваются следующие параметры:
method определяет команду действия HTTP POST.
body определяет текст запроса в представлении JSON. Код JSON создается
путем передачи литерала объекта, хранящегося в item , в функцию
JSON.stringify .
headers определяет заголовки Accept и Content-Type запросов HTTP. Для
обеих параметров устанавливается значение application/json , чтобы
классифицировать тип носителя при получении и отправке соответственно.
HTTP-запрос POST направляется по пути api/todoitems.
JavaScript
function addItem() {
const addNameTextbox = document.getElementById('add-name');
const item = {
isComplete: false,
name: addNameTextbox.value.trim()
};
fetch(uri, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(response => response.json())
.then(() => {
getItems();
addNameTextbox.value = '';
})
.catch(error => console.error('Unable to add item.', error));
}
Когда веб-API возвращает код состояния, указывающий на успешное выполнение,
вызывается функция getItems для обновления таблицы HTML. Если запрос веб-API
завершается сбоем, в консоли браузера регистрируется сообщение об ошибке.
Обновление элемента задачи
Обновление элемента списка задач аналогично его добавлению. Но есть два
существенных отличия:
Путь имеет суффикс с уникальным идентификатором обновляемого элемента.
Например, api/todoitems/1.
Команда действия HTTP — это PUT, как указано в параметре method .
JavaScript
fetch(`${uri}/${itemId}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(item)
})
.then(() => getItems())
.catch(error => console.error('Unable to update item.', error));
Удаление элемента задачи
Чтобы удалить элемент списка задач, укажите для параметра запроса method
значение DELETE и определите уникальный идентификатор элемента в URL-адресе.
JavaScript
fetch(`${uri}/${id}`, {
method: 'DELETE'
})
.then(() => getItems())
.catch(error => console.error('Unable to delete item.', error));
Перейдите к следующему руководству, в котором описано, как создавать страницы
справки по веб-API:
Начало работы с Swashbuckle и ASP.NET Core
Создание внутренних служб для
собственных мобильных приложений
в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 7 мин
Джеймс Монтемагно
Мобильные приложения могут взаимодействовать с внутренними службами
ASP.NET Core. Инструкции по подключению локальных веб-служб из симуляторов
iOS и эмуляторов Android см. в статье о подключении к локальным веб-службам из
симуляторов iOS и эмуляторов Android.
Просмотреть или скачать пример кода внутренней службы
Пример собственного мобильного
приложения
В этом руководстве показано, как создавать внутренние службы с помощью
ASP.NET Core для поддержки собственных мобильных приложений. Он использует
приложение TodoRest Xamarin.Forms в качестве собственного клиента, которое
включает отдельные собственные клиенты для Android, iOS и Windows. Вы можете
следовать приложенному руководству, чтобы создать собственное приложение (и
установить необходимые бесплатные средства Xamarin), а также скачать пример
решения Xamarin. Пример Xamarin включает проект служб веб-API ASP.NET Core,
который ASP.NET Core приложение этой статьи заменяет (без изменений,
необходимых клиенту).
Компоненты
Приложение TodoREST
поддерживает перечисление, добавление, удаление и
обновление To-Do элементов. Каждый элемент имеет идентификатор, имя, заметки
и свойство, указывающее, выполнен ли он.
Основное представление элементов, как показано выше, выводит список с именем
каждого элемента, указывая, выполнен ли он, с помощью флажка.
Если выбрать значок + , открывается диалоговое окно добавления элемента:
При выборе элемента на экране основного списка открывается диалоговое окно
редактирования, где можно изменить параметры имени, заметок и готовности
элемента, а также удалить его:
Чтобы протестировать его самостоятельно с помощью приложения ASP.NET Core,
созданного в следующем разделе, работающем на компьютере, обновите
константу RestUrl
приложения.
Эмуляторы Android не выполняются на локальном компьютере и используют IPадрес замыкания на себя (10.0.2.2) для взаимодействия с локальным компьютером.
Используйте Xamarin.Essentials DeviceInfo , чтобы определить, какая операционная
система работает, чтобы использовать правильный URL-адрес.
Перейдите к TodoREST
проекту и откройте Constants.cs
Constants.cs содержит следующую конфигурацию.
файл. Файл
C#
using Xamarin.Essentials;
using Xamarin.Forms;
namespace TodoREST
{
public static class Constants
{
// URL of REST service
//public static string RestUrl =
"https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";
// URL of REST service (Android does not use localhost)
// Use http cleartext for local deployment. Change to https for
production
public static string RestUrl = DeviceInfo.Platform ==
DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" :
"http://localhost:5000/api/todoitems/{0}";
}
}
При необходимости можно развернуть веб-службу в облачной службе, такой как
Azure, и обновить . RestUrl
Создание проекта ASP.NET Core
Создайте веб-приложение ASP.NET Core в Visual Studio. Выберите шаблон веб-API.
Назовите проект TodoAPI.
Приложение должно отвечать на все запросы, сделанные на порт 5000, включая
трафик HTTP для нашего мобильного клиента. Обновите Startup.cs так, чтобы
UseHttpsRedirection не выполнялось в разработке:
C#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// For mobile apps, allow http traffic.
app.UseHttpsRedirection();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
7 Примечание
Запустите приложение напрямую, а не за IIS Express. IIS Express игнорирует не
локальные запросы по умолчанию. Запустите dotnet run из командной строки
или выберите профиль имени приложения в раскрывающемся списке
"Целевой объект отладки" на панели инструментов Visual Studio.
Добавьте класс модели для представления элементов задач. Пометьте
обязательные поля с помощью атрибута [Required] :
C#
using System.ComponentModel.DataAnnotations;
namespace TodoAPI.Models
{
public class TodoItem
{
[Required]
public string ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Notes { get; set; }
public bool Done { get; set; }
}
}
Методам API нужен определенный способ для работы с данными. Используйте тот
же интерфейс ITodoRepository , который использует исходный пример Xamarin:
C#
using System.Collections.Generic;
using TodoAPI.Models;
namespace TodoAPI.Interfaces
{
public interface ITodoRepository
{
bool DoesItemExist(string id);
IEnumerable<TodoItem> All { get; }
TodoItem Find(string id);
void Insert(TodoItem item);
void Update(TodoItem item);
void Delete(string id);
}
}
В этом примере реализация использует просто частную коллекцию элементов:
C#
using
using
using
using
System.Collections.Generic;
System.Linq;
TodoAPI.Interfaces;
TodoAPI.Models;
namespace TodoAPI.Services
{
public class TodoRepository : ITodoRepository
{
private List<TodoItem> _todoList;
public TodoRepository()
{
InitializeData();
}
public IEnumerable<TodoItem> All
{
get { return _todoList; }
}
public bool DoesItemExist(string id)
{
return _todoList.Any(item => item.ID == id);
}
public TodoItem Find(string id)
{
return _todoList.FirstOrDefault(item => item.ID == id);
}
public void Insert(TodoItem item)
{
_todoList.Add(item);
}
public void Update(TodoItem item)
{
var todoItem = this.Find(item.ID);
var index = _todoList.IndexOf(todoItem);
_todoList.RemoveAt(index);
_todoList.Insert(index, item);
}
public void Delete(string id)
{
_todoList.Remove(this.Find(id));
}
private void InitializeData()
{
_todoList = new List<TodoItem>();
var todoItem1 = new TodoItem
{
ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
Name = "Learn app development",
Notes = "Take Microsoft Learn Courses",
Done = true
};
var todoItem2 = new TodoItem
{
ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
Name = "Develop apps",
Notes = "Use Visual Studio and Visual Studio for Mac",
Done = false
};
var todoItem3 = new TodoItem
{
ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
Name = "Publish apps",
Notes = "All app stores",
Done = false,
};
_todoList.Add(todoItem1);
_todoList.Add(todoItem2);
_todoList.Add(todoItem3);
}
}
}
Настройка реализации в Startup.cs :
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITodoRepository, TodoRepository>();
services.AddControllers();
}
Создание контроллера
Добавьте новый контроллер в проект TodoItemsController
. Он должен
наследовать от ControllerBase. Добавьте атрибут Route , чтобы указать, что
контроллер будет обрабатывать запросы, выполняемые по путям, начинающимся с
api/todoitems . Токен [controller] в маршруте заменяется на имя контроллера
(суффикс Controller опускается) и особенно удобен для глобальных маршрутов.
Узнайте больше о маршрутизации.
Для работы контроллеру нужен ITodoRepository ; запросите экземпляр этого типа
через конструктор контроллера. Во время выполнения этот экземпляр будет
предоставляться с помощью поддержки внедрения зависимостей на платформе.
C#
[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
private readonly ITodoRepository _todoRepository;
public TodoItemsController(ITodoRepository todoRepository)
{
_todoRepository = todoRepository;
}
Этот API поддерживает четыре различных HTTP-команды для выполнения
операций CRUD (создание, чтение, обновление, удаление) с источником данных.
Самой простой из них является операция чтения, которая соответствует HTTPзапросу GET.
Чтение элементов
Запрос списка элементов, выполняется с помощью отправки запроса GET в метод
List . Атрибут [HttpGet] в методе List указывает, что это действие должно
обрабатывать только запросы GET. Маршрут для этого действия соответствует
маршруту, указанному на контроллере. Использовать имя действия в составе
маршрута необязательно. Нужно лишь убедиться, что каждое действие имеет
уникальный и однозначный маршрут. Атрибуты маршрутизации можно применять
на уровне как контроллера, так и метода для создания определенных маршрутов.
C#
[HttpGet]
public IActionResult List()
{
return Ok(_todoRepository.All);
}
Метод List возвращает код отклика 200 OK и все элементы Todo, сериализованные
как JSON.
Вы можете проверить новый метод API с помощью различных средств, таких как
Postman , как показано ниже:
Создание элементов
По соглашению создание элементов данных сопоставляется с HTTP-командой
POST. Метод Create имеет примененный к нему атрибут [HttpPost] и принимает
экземпляр TodoItem . Так как аргумент item передается в текст POST, этот параметр
указывает атрибут [FromBody] .
Внутри метода элемент проверяется на допустимость и предшествующее
существование в хранилище данных, а при отсутствии проблем он добавляется с
помощью репозитория. Проверка ModelState.IsValid приводит к проверке модели
и должна выполняться в каждом методе API, принимающем вводимые
пользователем данные.
C#
[HttpPost]
public IActionResult Create([FromBody]TodoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return
BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
bool itemExists = _todoRepository.DoesItemExist(item.ID);
if (itemExists)
{
return StatusCode(StatusCodes.Status409Conflict,
ErrorCode.TodoItemIDInUse.ToString());
}
_todoRepository.Insert(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
}
return Ok(item);
}
В примере используется enum содержащий коды ошибок, передаваемые
мобильному клиенту:
C#
public enum ErrorCode
{
TodoItemNameAndNotesRequired,
TodoItemIDInUse,
RecordNotFound,
CouldNotCreateItem,
CouldNotUpdateItem,
CouldNotDeleteItem
}
Протестируйте добавление новых элементов с помощью Postman, выбрав команду
POST, предоставляющую новый объект в JSформате ON в тексте запроса. Вам
также нужно добавить заголовок запроса, указав Content-Type типа
application/json .
Метод возвращает вновь созданный элемент в отклике.
Обновление элементов
Изменение записей осуществляется с помощью HTTP-запросов PUT. За
исключением этого изменения, метод Edit практически идентичен Create .
Обратите внимание, что если запись не найдена, то действие Edit возвратит
отклик NotFound (404).
C#
[HttpPut]
public IActionResult Edit([FromBody] TodoItem item)
{
try
{
if (item == null || !ModelState.IsValid)
{
return
BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
}
var existingItem = _todoRepository.Find(item.ID);
if (existingItem == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_todoRepository.Update(item);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
}
return NoContent();
}
Чтобы выполнить проверку с помощью Postman, измените команду на PUT.
Укажите обновленные данные объекта в тексте запроса.
Этот метод возвращает отклик NoContent (204) при успешном выполнении, чтобы
обеспечить согласованность с ранее существовавшими API.
Удаление элементов
Удаление записей сопровождается отправкой запросов DELETE в службу и
передачей идентификатора удаляемого элемента. Как и в случае с обновлениями,
запросы несуществующих элементов будут получать отклики NotFound . В
противном случае успешный запрос получит отклик NoContent (204).
C#
[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
try
{
var item = _todoRepository.Find(id);
if (item == null)
{
return NotFound(ErrorCode.RecordNotFound.ToString());
}
_todoRepository.Delete(id);
}
catch (Exception)
{
return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
}
return NoContent();
}
Обратите внимание, что при тестировании функции удаления в тексте запроса не
требуется никаких элементов.
Предотвращение избыточной публикации
В настоящее время пример приложения предоставляет весь объект TodoItem .
Рабочие приложения обычно ограничивают вводимые данные и возвращают их с
помощью подмножества модели. Это связано с несколькими причинами, и
безопасность является основной. Подмножество модели обычно называется
объектом передачи данных (DTO), моделью ввода или моделью представления. В
этой статье используется DTO.
DTO можно использовать для следующего:
Предотвращение избыточной публикации.
Скрытие свойств, которые не предназначены для просмотра клиентами.
Пропуск некоторых свойств, чтобы уменьшить размер полезной нагрузки.
Сведение графов объектов, содержащих вложенные объекты. Сведенные
графы объектов могут быть удобнее для клиентов.
Чтобы продемонстрировать подход DTO, см. раздел "Предотвращение чрезмерной
публикации"
Общие соглашения веб-API
При разработке внутренних служб для своего приложения вам потребуется
согласованный набор соглашений или политик для обработки сквозных задач.
Например, в приведенной выше службе на запросы запрашивает определенные
записи, которые не были найдены, и был получен отклик NotFound , а не BadRequest .
Аналогичным образом, команды, выполненные для этой службы, которые
передавались в привязанные типы модели, всегда проверяли ModelState.IsValid и
возвращали BadRequest для недопустимых типов модели.
Определив общую политику для своих API, вы обычно можете инкапсулировать ее
в фильтр. Дополнительные сведения о том, как инкапсулировать общие политики
API в приложения ASP.NET Core MVC.
Дополнительные ресурсы
Xamarin.Forms: проверка подлинности веб-службы
Xamarin.Forms: использование RESTful веб-службы
Использование REST веб-служб в Xamarin Apps
Создание веб-API с помощью ASP.NET Core
Публикация веб-API ASP.NET Core в
службе управления API Azure с
помощью Visual Studio
Статья • 28.01.2023 • Чтение занимает 3 мин
Автор: Мэтт Сукуп
(Matt Soucoup)
В этом руководстве вы узнаете, как создать проект веб-API ASP.NET Core с
помощью Visual Studio, обеспечить поддержку OpenAPI, а затем опубликовать вебAPI как в Служба приложений Azure, так и в Azure Управление API.
Настройка
Для работы с этим руководством вам потребуется учетная запись Azure.
Создайте бесплатную учетную запись Azure
, если у вас ее нет.
Создание веб-API ASP.NET Core
Visual Studio позволяет легко создать проект веб-API ASP.NET Core на основе
шаблона. Следуйте этим инструкциям, чтобы создать проект веб-API ASP.NET Core:
В меню Файл выберите Создать >Проект.
В поле поиска введите Веб-API.
Выберите шаблон Веб-API ASP.NET Core и нажмите кнопку Далее.
В диалоговом окне Настройка нового проекта присвойте проекту имя
WeatherAPI и нажмите кнопку Далее.
В диалоговом окне Дополнительные сведения выполните следующие
действия.
Убедитесь, что для параметра Платформа выбрано значение .NET 6.0
(долгосрочная поддержка).
Убедитесь, что установлен флажок Использовать контроллеры (снимите
флажок для использования минимальных API).
Убедитесь, что установлен флажок Включить поддержку OpenAPI .
Нажмите кнопку создания.
Обзор кода
Определения Swagger позволяют Управление API Azure считывать определения API
приложения. Установив флажок Включить поддержку OpenAPI во время создания
приложения, Visual Studio автоматически добавляет код для создания определений
Swagger. Откройте файл со Program.cs следующим кодом:
C#
...
builder.Services.AddSwaggerGen();
...
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
...
Убедитесь, что определения Swagger всегда создаются
Azure Управление API требует, чтобы определения Swagger всегда присутствовали
независимо от среды приложения. Чтобы убедиться, что они всегда создаются,
переместите app.UseSwagger(); за пределы if (app.Environment.IsDevelopment())
блока.
Обновленный код выглядит следующим образом:
C#
...
app.UseSwagger();
if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI();
}
...
Изменение маршрутизации API
Измените структуру URL-адреса, необходимую Get для доступа к действию
WeatherForecastController . Выполните следующие шаги:
1. Откройте файл WeatherForecastController.cs .
2. Замените [Route("[controller]")] атрибут уровня класса на [Route("/")] .
Обновленное определение класса :
C#
[ApiController]
[Route("/")]
public class WeatherForecastController : ControllerBase
Публикация веб-API в Службе приложений
Azure
Выполните следующие действия, чтобы опубликовать веб-API ASP.NET Core в
службе управления API Azure.
1. Публикация приложения API в Службе приложений Azure
2. Опубликуйте приложение веб-API ASP.NET Core в экземпляре службы
управления API Azure.
Публикация приложения API в Службе приложений
Azure
Выполните следующие действия, чтобы опубликовать веб-API ASP.NET Core в
службе управления API Azure.
1. Щелкните правой кнопкой мыши проект в обозревателе решений и
выберите пункт Опубликовать.
2. В диалоговом окне Публикация выберите Azure и нажмите кнопку Далее .
3. Выберите Служба приложений Azure (Windows) и нажмите кнопку Далее.
4. Выберите Создание новой службы приложений Azure.
Откроется диалоговое окно Создать службу приложений. Заполняются поля
ввода Имя приложения, Группа ресурсов и План службы приложений. Вы
можете сохранить эти имена или изменить их.
5. Нажмите кнопку Создать.
6. После создания службы приложений нажмите кнопку Далее .
7. Выберите Создать новую службу Управление API.
Откроется диалоговое окно Создание службы Управление API. Можно
оставить поля Имя API, Имя подписки и Группа ресурсов . Нажмите кнопку
создать рядом с записью Управление API Service (Служба Управление API) и
введите необходимые поля в этом диалоговом окне.
Нажмите кнопку ОК, чтобы создать службу Управление API.
8. Нажмите кнопку Создать, чтобы продолжить создание службы Управление
API. Этот шаг может занять несколько минут.
9. По завершении нажмите кнопку Готово .
10. Диалоговое окно закроется, а затем появится экран сводки со сведениями о
публикации. Нажмите кнопку Опубликовать.
Веб-API публикуется как в Служба приложений Azure, так и в azure
Управление API. Появится новое окно браузера, в котором будет показан API,
работающий в службе приложений Azure. Это окно можно закрыть.
11. Откройте портал Azure в веб-браузере и перейдите к созданному экземпляру
Управление API.
12. Выберите параметр API в меню слева.
13. Выберите API, созданный на предыдущих шагах. Теперь он заполнен, и вы
можете изучить его.
Настройка имени опубликованного API
Обратите внимание, что имя API называется WeatherAPI; тем не менее, мы хотели
бы назвать это прогнозы погоды. Чтобы обновить имя, выполните следующие
действия.
1. Добавьте следующий код в сразу Program.cs после servies.AddSwaggerGen();
C#
builder.Services.ConfigureSwaggerGen(setup =>
{
setup.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Weather Forecasts",
Version = "v1"
});
});
2. Повторно опубликуйте веб-API ASP.NET Core и откройте экземпляр службы
управления API Azure на портале Azure.
3. Обновите страницу в браузере. Теперь отобразится правильное имя API.
Проверка работоспособности веб-API
Вы можете протестировать развернутый веб-API ASP.NET Core в службе
управления API Azure на портале Azure, выполнив следующие действия.
1. Откройте вкладку Тест.
2. Выберите / или операцию Get.
3. Нажмите кнопку Отправить.
Очистка
Завершив тестирование приложения, перейдите на портал Azure
и удалите
приложение.
1. Выберите пункт Группы ресурсов, а затем созданную группу ресурсов.
2. На странице Группы ресурсов выберите Удалить.
3. Введите имя группы ресурсов и выберите Удалить. Ваше приложение и все
ресурсы, созданные при работе с этим руководством, удалены из Azure.
Дополнительные ресурсы
Управление API Azure
Служба приложений Azure
Учебник. Начало работы с SignalR
ASP.NET Core
Статья • 02.12.2022 • Чтение занимает 12 мин
В этом учебнике описаны основы создания приложения, работающего в режиме
реального времени, с помощью SignalR. Вы научитесь:
" Создайте веб-проект.
" добавлять клиентскую библиотеку SignalR.
" создавать концентратор SignalR.
" настраивать проект для использования SignalR;
" Добавлять код для отправки сообщений из любого клиента всем
подключенным клиентам.
В итоге вы получите работающее приложение чата:
Предварительные требования
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создание проекта веб-приложения
Visual Studio
1. Запустите Visual Studio 2022 и нажмите Создать проект.
2. В диалоговом окне Создать проект выберите Веб-приложение
ASP.NET Core и нажмите Далее.
3. В диалоговом окне Настроить новый проект введите SignalRChat в поле
Имя проекта. Важно присвоить проекту имя SignalRChat, включая
сопоставление регистра букв, чтобы обеспечить соответствие
пространств имен при копировании и вставке примера кода.
4. Выберите Далее.
5. В диалоговом окне Дополнительные сведения выберите .NET 6.0
(долгосрочная поддержка) и щелкните Создать.
Добавление клиентской библиотеки SignalR
Серверная библиотека SignalR входит в состав общей платформы ASP.NET Core.
Клиентская библиотека JavaScript не добавляется в проект автоматически. В этом
руководстве показано, как использовать диспетчер библиотек (LibMan), чтобы
получить клиентскую библиотеку из unpkg
. unpkg это быстрая глобальная сеть
доставки содержимого для всего в npm .
Visual Studio
В обозревателе решений щелкните проект правой кнопкой мыши и
выберите Добавить>Client-Side Library (Клиентская библиотека).
В диалоговом окне Добавление библиотеки на стороне клиента:
Выберите unpkg для параметра Поставщик.
Введите @microsoft/signalr@latest для параметра Библиотека.
Щелкните Choose specific files (Выбрать определенные файлы),
разверните папку dist/browser и выберите signalr.js и signalr.min.js .
В поле Целевое расположение укажите wwwroot/js/signalr/.
Щелкните Установить.
LibMan создает папку wwwroot/js/signalr и копирует в нее выбранные файлы.
Создание концентратора SignalR
hub — это класс, который служит в качестве конвейера высокого уровня для
обработки взаимодействия между клиентом и сервером.
В папке проекта SignalRChat создайте папку Hubs.
В папке Hubs создайте класс ChatHub , содержащий следующий код:
C#
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
Класс ChatHub наследуется от класса SignalRHub. Класс Hub управляет
подключениями, группами и обменом сообщениями.
Метод SendMessage может вызываться подключенным клиентом, чтобы отправить
сообщение всем клиентам. Далее в этом учебника показан клиентский код
JavaScript, который вызывает метод. Код SignalR является асинхронным, поэтому
обеспечивает максимальную масштабируемость.
Настройка SignalR
Сервер SignalR должен быть настроен для передачи запросов SignalR к SignalR.
Добавьте следующий выделенный код в файл Program.cs :
C#
using SignalRChat.Hubs;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();
Выделенный выше код добавляет SignalR к системам маршрутизации и внедрения
зависимостей ASP.NET Core.
Добавление клиентского кода SignalR
Замените все содержимое в Pages/Index.cshtml следующим кодом:
CSHTML
@page
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-2">User</div>
<div class="col-4"><input type="text" id="userInput" />
</div>
</div>
<div class="row">
<div class="col-2">Message</div>
<div class="col-4"><input type="text" id="messageInput" />
</div>
</div>
<div class="row"> </div>
<div class="row">
<div class="col-6">
<input type="button" id="sendButton" value="Send
Message" />
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
Предыдущая разметка:
Создает текстовые поля и кнопку отправки.
Создает список с id="messagesList" для отображения сообщений,
полученных от концентратора SignalR.
Содержит ссылки на скрипты для SignalR и код приложения chat.js ,
который создается на следующем шаге.
В папке wwwroot/js создайте файл chat.js со следующим кодом:
JavaScript
"use strict";
var connection = new
signalR.HubConnectionBuilder().withUrl("/chatHub").build();
//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var li = document.createElement("li");
document.getElementById("messagesList").appendChild(li);
// We can assign user-supplied strings to an element's textContent
because it
// is not interpreted as markup. If you're assigning in any other
way, you
// should be aware of possible script injection concerns.
li.textContent = `${user} says ${message}`;
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click",
function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function
(err) {
return console.error(err.toString());
});
event.preventDefault();
});
Предыдущий код JavaScript:
Создает и запускает подключение.
Добавляет к кнопке отправки обработчик, который отправляет сообщения
в концентратор.
Добавляет к объекту подключения обработчик, который получает
сообщения из концентратора и добавляет их в список.
Запуск приложения
Visual Studio
Нажмите клавиши CTRL+F5, чтобы запустить приложение без отладки.
Скопируйте URL-адрес из адресной строки, откройте другой экземпляр или
вкладку браузера и вставьте URL-адрес в адресную строку.
Выберите любой браузер, введите имя и сообщение и нажмите кнопку
Отправить сообщение. Имя и сообщение отображаются на обеих страницах
мгновенно.
 Совет
Если приложение не работает, откройте средства разработчика для
браузера (F12) и перейдите в консоль. Вы можете увидеть ошибки,
связанные с вашим кодом HTML и JavaScript. Предположим, вы
поместили signalr.js не в ту папку, которую указали. В этом случае
ссылка на этот файл не будет работать, и вы увидите сообщение об
ошибке 404 в консоли.
Если возникает ошибка ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY в
Chrome, выполните эти команды, чтобы обновить сертификат разработки:
Интерфейс командной строки.NET
dotnet dev-certs https --clean
dotnet dev-certs https --trust
Публикация в Azure
Сведения о развертывании в Azure см. в разделе Краткое руководство.
Развертывание веб-приложения ASP.NET.
Учебник. Начало работы с ASP.NET
Core SignalR с использованием
TypeScript и Webpack.
Статья • 28.01.2023 • Чтение занимает 29 мин
Авторы: Себастьен Сунье (Sébastien Sougnez)
и Скотт Эдди (Scott Addie)
В этом учебнике демонстрируется использование средства Webpack
для
создания пакета и сборки веб-приложения ASP.NET Core SignalR, а также
объединения и создания клиента, который написан на языке TypeScript . С
помощью средства Webpack разработчики могут создавать пакеты и выполнять
сборку ресурсов на стороне клиента для веб-приложения.
В этом руководстве описано следующее:
" Создание приложения ASP.NET Core SignalR
" настроить сервер SignalR.
" Настроить конвейер сборки с использованием Webpack
" настроить клиент TypeScript SignalR;
" Включение обмена данными между клиентом и сервером.
Просмотреть или скачать образец кода
(как скачивать)
Предварительные требования
Node.js
с npm
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создание веб-приложения ASP.NET Core
Visual Studio
По умолчанию Visual Studio использует версию npm, которая находится в его
каталоге установки. Настройка Visual Studio на поиск npm в переменной среды
PATH :
1. Запустите Visual Studio. В начальном окне выберите Продолжить без
кода.
2. Щелкните Сервис>Параметры>Проекты и решения>Управление вебпакетами>Внешние веб-инструменты.
3. Выберите элемент $(PATH) в списке. Щелкните стрелку вверх, чтобы
переместить этот элемент на вторую позицию в списке, и нажмите OK.
Создание веб-приложения ASP.NET Core:
1. Перейдите в меню Файл>Создать>Проект и выберите шаблон Пустой
шаблон ASP.NET Core. Выберите Далее.
2. Задайте для проекта имя SignalRWebpack и нажмите кнопку Создать.
3. Выберите .NET 6.0 (Long-term support) в раскрывающемся меню
Платформа. Щелкните Создать.
Добавьте в проект пакет NuGet Microsoft.TypeScript.MSBuild .
1. В обозревателе решений щелкните правой кнопкой мыши узел проекта и
выберите Управление пакетами NuGet. На вкладке Обзор найдите
Microsoft.TypeScript.MSBuild , а затем нажмите кнопку Установить справа,
чтобы установить пакет.
Visual Studio добавляет пакет NuGet в узел Зависимости в обозревателе
решений, разрешая компиляцию TypeScript в проекте.
Настройка сервера
В этом разделе вы настроите веб-приложение ASP.NET Core для отправки и
получения сообщений SignalR.
1. В Program.cs вызовите AddSignalR.
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
2. В Program.cs снова вызовите UseDefaultFiles и UseStaticFiles:
C#
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
Приведенный выше код позволяет серверу размещать и обслуживать файл
index.html . Файл обрабатывается независимо от того, вводит ли пользователь
полный или корневой URL-адрес веб-приложения.
3. Создайте новый каталог с именем Hubs в корневом каталоге проекта
SignalRWebpack/ для хранения класса концентратора SignalR.
4. Создайте файл Hubs/ChatHub.cs со следующим кодом:
C#
using Microsoft.AspNetCore.SignalR;
namespace SignalRWebpack.Hubs;
public class ChatHub : Hub
{
public async Task NewMessage(long username, string message) =>
await Clients.All.SendAsync("messageReceived", username,
message);
}
Приведенный выше код осуществляет широковещательную рассылку
полученных сервером сообщений всем подключенным пользователям.
Универсальный метод on для получения всех сообщений не требуется. Имя
метода указывается после суффиксов имени сообщения.
В этом примере клиент TypeScript отправляет сообщение,
идентифицированное как newMessage . Метод C# NewMessage ожидает данные,
отправленные клиентом. Выполняется вызов метода SendAsync для Clients.All.
Полученные сообщения отправляются всем клиентам, подключенным к
концентратору.
5. Добавьте следующий оператор using в самое начало Program.cs , чтобы
разрешить ссылку ChatHub :
C#
using SignalRWebpack.Hubs;
6. В Program.cs сопоставьте маршрут /hub с концентратором ChatHub . Замените
код, отображающий сообщение Hello World! , следующим кодом:
C#
app.MapHub<ChatHub>("/hub");
Настройка клиента
В этом разделе вы создадите проект Node.js
для преобразования TypeScript в
JavaScript и объединения ресурсов на стороне клиента, включая HTML и CSS, с
помощью Webpack.
1. В корневом элементе проекта выполните следующую команду, чтобы создать
файл package.json :
Консоль
npm init -y
2. Добавьте выделенное свойство в файл package.json и сохраните изменения:
JSON
{
"name": "SignalRWebpack",
"version": "1.0.0",
"private": true,
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Если свойству private присвоено значение true , на следующем шаге не будут
отображаться предупреждения об установке пакета.
3. Установите необходимые пакеты npm. Выполните следующую команду в
корневом элементе проекта:
Консоль
npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin minicss-extract-plugin ts-loader typescript webpack webpack-cli
Использование параметра -E позволяет отключить установленное по
умолчанию поведение npm, предусматривающее запись операторов
диапазона семантического управления версиями
в файл package.json .
Например, "webpack": "5.70.0" используется вместо "webpack": "^5.70.0" .
Этот параметр позволяет исключить непреднамеренное обновление до более
новых версий пакета.
Дополнительные сведения см. в npm-install
.
4. Замените свойство scripts в файле package.json следующим кодом:
JSON
"scripts": {
"build": "webpack --mode=development --watch",
"release": "webpack --mode=production",
"publish": "npm run release && dotnet publish -c Release"
},
Определены следующие скрипты:
build . Создает пакет ваших ресурсов на стороне клиента в режиме
разработки и отслеживает изменения. Наблюдатель за файлами
выполняет повторное создание пакета при каждом изменении файла
проекта. Параметр mode позволяет отключить оптимизации в рабочей
среде, такие как встряхивание дерева и минификация. build
используется только в среде разработки.
release . Создает пакет ресурсов на стороне клиента в рабочем режиме.
publish . запускает скрипт release для создания пакета ресурсов на
стороне клиента в рабочем режиме. Этот скрипт вызывает команду
publish интерфейса командной строки .NET для публикации приложения.
5. Создайте в корневом элементе проекта файл webpack.config.js со следующим
кодом:
JavaScript
const
const
const
const
path = require("path");
HtmlWebpackPlugin = require("html-webpack-plugin");
{ CleanWebpackPlugin } = require("clean-webpack-plugin");
MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "wwwroot"),
filename: "[name].[chunkhash].js",
publicPath: "/",
},
resolve: {
extensions: [".js", ".ts"],
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
new MiniCssExtractPlugin({
filename: "css/[name].[chunkhash].css",
}),
],
};
Приведенный выше файл определяет конфигурацию процесса компиляции
Webpack.
Свойство output переопределяет значение по умолчанию для dist .
Вместо этого пакет выводится в каталог wwwroot .
Массив resolve.extensions содержит элемент .js для импорта кода
JavaScript клиента SignalR.
6. Скопируйте каталог src из примера проекта
в корневой каталог проекта.
Каталог src содержит следующие файлы:
index.html , который определяет стереотипную разметку главной
страницы.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>ASP.NET Core SignalR with TypeScript and
Webpack</title>
</head>
<body>
<div id="divMessages" class="messages"></div>
<div class="input-zone">
<label id="lblMessage" for="tbMessage">Message:</label>
<input id="tbMessage" class="input-zone-input" type="text"
/>
<button id="btnSend">Send</button>
</div>
</body>
</html>
css/main.css , который предоставляет стили CSS для главной страницы:
css
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
.input-zone {
align-items: center;
display: flex;
flex-direction: row;
margin: 10px;
}
.input-zone-input {
flex: 1;
margin-right: 10px;
}
.message-author {
font-weight: bold;
}
.messages {
border: 1px solid #000;
margin: 10px;
max-height: 300px;
min-height: 300px;
overflow-y: auto;
padding: 5px;
}
tsconfig.json , который определяет конфигурацию компилятора
TypeScript для получения совместимого с ECMAScript
JSON
{
"compilerOptions": {
"target": "es5"
}
}
index.ts :
TypeScript
import * as signalR from "@microsoft/signalr";
import "./css/main.css";
const divMessages: HTMLDivElement =
document.querySelector("#divMessages");
5 кода JavaScript.
const tbMessage: HTMLInputElement =
document.querySelector("#tbMessage");
const btnSend: HTMLButtonElement =
document.querySelector("#btnSend");
const username = new Date().getTime();
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub")
.build();
connection.on("messageReceived", (username: string, message:
string) => {
const m = document.createElement("div");
m.innerHTML = `<div class="message-author">${username}</div>
<div>${message}</div>`;
divMessages.appendChild(m);
divMessages.scrollTop = divMessages.scrollHeight;
});
connection.start().catch((err) => document.write(err));
tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
if (e.key === "Enter") {
send();
}
});
btnSend.addEventListener("click", send);
function send() {
connection.send("newMessage", username, tbMessage.value)
.then(() => (tbMessage.value = ""));
}
Приведенный выше код извлекает ссылки на элементы модели DOM и
присоединяет два обработчика событий:
keyup : срабатывает, когда пользователь вводит данные в текстовое
поле tbMessage , и вызывает функцию send при нажатии
пользователем клавиши Enter.
click : срабатывает, когда пользователь нажимает кнопку Отправить и
вызывается функцию send .
Класс HubConnectionBuilder создает новый построитель для настройки
подключения к серверу. Функция withUrl настраивает URL-адрес
концентратора.
SignalR обеспечивает обмен сообщениями между клиентом и сервером.
Каждому сообщению присваивается определенное имя. Например,
сообщения с именем messageReceived могут выполнять логику
отображения новых сообщений в соответствующей зоне.
Прослушивание определенных сообщений реализуется с помощью
функции on . Возможно прослушивание любого числа имен сообщений.
Кроме того, можно передавать параметры сообщения, например имя его
автора или содержимое полученного сообщения. После того как клиент
получает сообщение, создается новый элемент div , в атрибуте innerHTML
которого содержатся имя автора и содержимое сообщения. Этот
элемент добавляется в основной элемент div , который используется для
отображения сообщений.
Для отправки сообщения через соединение WebSockets необходимо
вызвать метод send . Первый параметр этого метода содержит имя
сообщения. Другие параметры заполняются данными сообщения. В этом
примере на сервер отправляется сообщение, идентифицированное как
newMessage . Это сообщение содержит имя пользователя, а также данные,
введенные этим пользователем в текстовое поле. Если отправка
выполняется успешно, значение текстового поля очищается.
7. Выполните следующую команду в корневом элементе проекта:
Консоль
npm i @microsoft/signalr @types/node
Предыдущая команда устанавливает:
Клиент TypeScript SignalR
, который позволяет клиенту отправлять
сообщения на сервер.
Определения типа TypeScript для Node.js, обеспечивающие проверку
типов Node.js во время компиляции.
Тестирование приложения
Чтобы проверить работоспособность приложения, выполните следующие шаги.
Visual Studio
1. Запустите средство Webpack в режиме release . Выполните следующие
команды в окне Консоль диспетчера пакетов в корневом элементе
проекта. Если вы не в корневом каталоге проекта, введите перед этой
командой cd SignalRWebpack .
Консоль
npm run release
Эта команда создает ресурсы на стороне клиента, которые будут
обслуживаться при выполнении приложения. Эти ресурсы помещаются в
папку wwwroot .
Веб-пакет выполнил следующие задачи:
Очистка содержимого каталога wwwroot .
Преобразование TypeScript в JavaScript (этот процесс называется
транспилированием).
Корректировка созданного кода JavaScript в целях уменьшения
размера файла (этот процесс называется минификацией).
Копирование обработанных файлов JavaScript, CSS и HTML из
каталога src в wwwroot .
Внедрение следующих элементов в файл wwwroot/index.html :
Тег <link> , ссылающийся на файл wwwroot/main.<hash>.css . Этот
тег размещается непосредственно после закрывающего тега
</head> .
Тег <script> , ссылающийся на минифицированный файл
wwwroot/main.<hash>.js . Этот тег размещается непосредственно
после закрывающего тега </body> .
2. Выберите Отладка>Запуск без отладки, чтобы запустить приложение в
браузере, не присоединяя отладчик. Файл wwwroot/index.html
обрабатывается по адресу https://localhost:<port> .
Если во время компиляции возникают ошибки, попробуйте закрыть и
снова открыть решение.
3. Откройте другой экземпляр браузера (любого) и вставьте URL-адрес в
адресную строку.
4. Выберите любой браузер, введите произвольный текст в поле
Сообщение и нажмите кнопку Отправить. На обеих страницах мгновенно
отображаются имя пользователя и сообщение.
Дополнительные ресурсы
Клиент JavaScript SignalR ASP.NET Core
Использование концентраторов в ASP.NET Core SignalR
Использование SignalR для ASP.NET
Core с Blazor
Статья • 28.11.2022 • Чтение занимает 57 мин
В этом руководстве описаны основы того, как с помощью SignalR и Blazor создать
приложение, работающее в режиме реального времени.
Вы узнаете, как выполнять следующие задачи:
" создавать проекты Blazor;
" Добавление клиентской библиотеки SignalR
" добавлять концентратор SignalR;
" добавлять службы и конечную точку SignalR для концентратора SignalR;
" добавлять код компонента Razor для чата.
Когда вы выполните задачи из этого руководства, у вас будет работающее
приложение чата.
Предварительные требования
Visual Studio
Visual Studio 2022 или более поздней версии с рабочей нагрузкой ASP.NET
и разработка веб-приложений
Пакет SDK для .NET 6.0
Пример приложения
Для работы с этим руководством загружать пример приложения чата не
обязательно. Пример приложения — это готовое рабочее приложение, созданное
в результате выполнения действий, описанных в этом учебнике.
Просмотреть или скачать образец кода
Создание приложения Blazor Server
Следуйте указаниям по выбору инструментов:
Visual Studio
7 Примечание
Требуются Visual Studio 2022 или более поздней версии и пакет SDK для
.NET Core 6.0.0 или более поздней версии.
1. Создайте новый проект.
2. Выберите шаблон Blazor ServerПриложение. Выберите Далее.
3. Введите BlazorServerSignalRApp в поле Имя проекта. Убедитесь, что для
проекта правильно указано существующее расположение или укажите
новое. Выберите Далее.
4. Нажмите кнопку создания.
Добавление клиентской библиотеки SignalR
Visual Studio
1. В обозревателе решений щелкните проект BlazorServerSignalRApp
правой кнопкой мыши и выберите пункт Управление пакетами NuGet.
2. Убедитесь, что в диалоговом окне Управление пакетами NuGet для
параметра Источник пакета установлено значение nuget.org .
3. Нажав кнопку Обзор, введите Microsoft.AspNetCore.SignalR.Client в поле
поиска.
4. В результатах поиска выберите пакет
Microsoft.AspNetCore.SignalR.Client . Задайте версию в соответствии с
общей платформой приложения. Выберите пункт Установить.
5. Если откроется диалоговое окно Просмотр изменений, нажмите кнопку
ОК.
6. Если откроется диалоговое окно Принятие условий лицензионного
соглашения, выберите Я принимаю, если принимаете условия.
добавлять концентратор SignalR;
Создайте папку Hubs (plural) и добавьте следующий класс ChatHub ( Hubs/ChatHub.cs ):
C#
using Microsoft.AspNetCore.SignalR;
namespace BlazorServerSignalRApp.Server.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
Добавление служб и конечной точки для
концентратора SignalR
1. Откройте файл Program.cs .
2. Добавьте пространства имен для Microsoft.AspNetCore.ResponseCompression и
класс ChatHub в начало файла:
C#
using Microsoft.AspNetCore.ResponseCompression;
using BlazorServerSignalRApp.Server.Hubs;
3. Добавьте службы промежуточного ПО для сжатия ответа в Program.cs :
C#
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
4. В Program.cs :
Используйте ПО промежуточного слоя для сжатия ответа в верхней
части конфигурации конвейера обработки.
Между конечными точками для сопоставления концентратора Blazor и
отката на стороне клиента добавьте конечную точку для концентратора.
C#
app.UseResponseCompression();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapHub<ChatHub>("/chathub");
app.MapFallbackToPage("/_Host");
app.Run();
добавлять код компонента Razor для чата.
1. Откройте файл Pages/Index.razor .
2. Замените разметку следующим кодом:
razor
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable
<PageTitle>Index</PageTitle>
<div class="form-group">
<label>
User:
<input @bind="userInput" />
</label>
</div>
<div class="form-group">
<label>
Message:
<input @bind="messageInput" size="50" />
</label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>
<hr>
<ul id="messagesList">
@foreach (var message in messages)
{
<li>@message</li>
}
</ul>
@code {
private
private
private
private
HubConnection? hubConnection;
List<string> messages = new List<string>();
string? userInput;
string? messageInput;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user,
message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
}
private async Task Send()
{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendMessage", userInput,
messageInput);
}
}
public bool IsConnected =>
hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
7 Примечание
Отключите ПО промежуточного слоя для сжатия ответов в среде Development
при использовании Горячей перегрузки. Дополнительные сведения см. в
статье Руководство по ASP.NET Core BlazorSignalR.
Запуск приложения
Следуйте указаниям по выбору инструментов:
Visual Studio
1. Нажмите клавишу
+
F5
F5
(Windows) либо
, чтобы запустить приложение с отладкой, или
⌘
+
F5
CTRL
(macOS), чтобы запустить его без отладки.
2. Скопируйте URL-адрес из адресной строки, откройте другой экземпляр
или вкладку браузера и вставьте URL-адрес в адресную строку.
3. Выберите любой браузер, введите имя и сообщение и нажмите кнопку
для отправки сообщения. Имя и сообщение отображаются на обеих
страницах мгновенно:
Цитаты: Звездный путь VI. Неоткрытая страна ©1991 Paramount
Дальнейшие действия
В этом руководстве вы узнали, как выполнять следующие задачи:
" создавать проекты Blazor;
" Добавление клиентской библиотеки SignalR
" добавлять концентратор SignalR;
" добавлять службы и конечную точку SignalR для концентратора SignalR;
" добавлять код компонента Razor для чата.
Дополнительные сведения о создании приложений Blazor см. в документации по
Blazor:
Проверка подлинности Blazor
маркера носителя с помощью сервера Identity, WebSockets и отправляемыми
сервером событиями
Дополнительные ресурсы
Безопасные концентраторы SignalR для размещенных приложений Blazor
WebAssembly
Общие сведения об ASP.NET CoreSignalR
Согласование независимо от источника для проверки подлинности для
SignalR
Конфигурация SignalR
Отладка в ASP.NET Core Blazor WebAssembly
Руководство по предотвращению угроз для ASP.NET Core Blazor Server
Репозиторий GitHub с примерами для Blazor (dotnet/blazor-samples)
Учебник. Создание клиента и сервера
gRPC в ASP.NET Core
Статья • 05.10.2022 • Чтение занимает 28 мин
В этом руководстве показано, как создать клиент gRPC в .NET Core и сервер gRPC
ASP.NET Core. В итоге вы получите клиент gRPC, который взаимодействует со
службой Greeter gRPC.
В этом учебнике рассмотрены следующие задачи.
" Создание сервера gRPC.
" Создание клиента gRPC.
" Тестирование клиента gRPC с помощью службы Greeter gRPC.
Предварительные требования
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Создание службы gRPC
Visual Studio
Запустите Visual Studio 2022 и нажмите Создать проект.
В диалоговом окне Создание нового проекта найдите пункт gRPC .
Выберите Служба ASP.NET Core gRPC и нажмите Далее.
В диалоговом окне Настроить новый проект введите GrpcGreeter в поле
Имя проекта. Для проекта необходимо установить имя GrpcGreeter, чтобы
при копировании и вставке кода совпадали пространства имен.
Выберите Далее.
В диалоговом окне Дополнительные сведения выберите .NET 6.0
(долгосрочная поддержка) и щелкните Создать.
Запуск службы
Visual Studio
Нажмите клавиши CTRL+F5, чтобы выполнить запуск без отладчика.
Visual Studio отображает следующее диалоговое окно, если проект еще не
настроен для использования SSL:
Выберите Да, чтобы сделать SSL-сертификат IIS Express доверенным.
Отобразится следующее диалоговое окно.
Выберите Да, если согласны доверять сертификату разработки.
Сведения о доверии к браузеру Firefox см. в разделе Ошибка сертификата
браузера Firefox SEC_ERROR_INADEQUATE_KEY_USAGE.
Visual Studio:
Запускает сервер Kestrel.
запускает браузер;
переходит к адресу http://localhost:port , например
http://localhost:7042 .
port: это номер порта, назначенный для приложения случайным
образом.
localhost : это стандартное имя узла для локального компьютера.
Localhost обслуживает только веб-запросы с локального
компьютера.
В журналах показана служба, ожидающая передачи данных на https://localhost:
<port> , где <port> — это номер порта localhost, назначаемый случайным образом
при создании проекта и задаваемый в Properties/launchSettings.json .
Консоль
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
7 Примечание
Шаблон gRPC настроен для использования протокола TLS . Клиенты gRPC
должны использовать протокол HTTPS для обращения к серверу. Номер порта
localhost службы gRPC назначается случайным образом при создании проекта
и задается в файле Properties\launchSettings.json проекта службы gRPC.
macOS не поддерживает ASP.NET Core gRPC с TLS. Для успешного запуска
служб gRPC в macOS требуется дополнительная настройка. Дополнительные
сведения см. в статье Не удается запустить приложение ASP.NET Core gRPC в
macOS.
Анализ файлов проекта
Файлы проекта GrpcGreeter:
Protos/greet.proto : определяет службу gRPC Greeter и используется для
создания ресурсов сервера gRPC. Дополнительные сведения см. в разделе
Введение в gRPC.
Папка Services : содержит реализацию службы Greeter .
appSettings.json : содержит данные конфигурации, включая протокол,
используемый в Kestrel. Дополнительные сведения см. в разделе
Конфигурация в ASP.NET Core.
Program.cs содержит следующее:
Точку входа для службы gRPC. Дополнительные сведения см. в статье
Универсальный узел .NET в ASP.NET Core.
Код, задающий поведение приложения. Дополнительные сведения: Запуск
приложения.
Создание клиента gRPC в консольном
приложении .NET
Visual Studio
Откройте второй экземпляр Visual Studio и щелкните Создать проект.
В диалоговом окне Создание проекта выберите Консольное приложение
и нажмите Далее.
В текстовое поле Имя проекта введите GrpcGreeterClient и нажмите
Далее.
В диалоговом окне Дополнительные сведения выберите .NET 6.0
(долгосрочная поддержка) и щелкните Создать.
Добавьте необходимые пакеты NuGet
Для клиентского проекта gRPC требуются следующие пакеты NuGet:
Grpc.Net.Client , который содержит клиент .NET Core.
Google.Protobuf , который содержит API сообщений protobuf для C#;
Grpc.Tools , который содержит поддержку инструментов C# для
файлов protobuf. Пакет инструментов не требуется во время выполнения,
поэтому зависимость помечается PrivateAssets="All" .
Visual Studio
Установка пакетов с помощью консоли диспетчера пакетов (PMC) или
управления пакетами NuGet.
Установка пакетов с помощью консоли диспетчера пакетов
В Visual Studio выберите пункты меню Сервис>Диспетчер пакетов
NuGet>Консоль диспетчера пакетов.
В консоли диспетчера пакетов выполните команду cd GrpcGreeterClient ,
чтобы перейти в папку с файлами GrpcGreeterClient.csproj .
Выполните следующие команды:
PowerShell
Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools
Установка пакетов с помощью раздела управления
пакетами NuGet
Щелкните правой кнопкой мыши проект в обозревателе
решений>Управление пакетами NuGet.
Выберите вкладку Обзор.
В поле поиска введите Grpc.Net.Client.
Выберите пакет Grpc.Net.Client на вкладке Обзор и нажмите кнопку
Установить.
Повторите все шаги для Google.Protobuf и Grpc.Tools .
Добавление greet.proto
Создайте папку Protos в клиентском проекте gRPC.
Скопируйте файл Protos\greet.proto из службы Greeter gRPC в папку Protos
проекта клиента gRPC.
Измените пространство имен в файле greet.proto на пространство имен
проекта:
option csharp_namespace = "GrpcGreeterClient";
Измените файл проекта GrpcGreeterClient.csproj :
Visual Studio
Щелкните проект правой кнопкой мыши и выберите Изменить файл проекта.
Добавьте группу элементов с элементом <Protobuf> , ссылающимся на файл
greet.proto:
XML
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>
Создание клиента Greeter
Скомпилируйте клиентский проект, чтобы создать типы в пространстве имен
GrpcGreeterClient .
7 Примечание
Типы GrpcGreeterClient создаются автоматически в процессе сборки. Пакет
инструментов Grpc.Tools
создает следующие файлы на основе файла
greet.proto:
GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\Greet.cs : код для
буфера протокола, который заполняет, сериализует и извлекает типы
сообщений для запроса и ответа;
GrpcGreeterClient\obj\Debug\[TARGET_FRAMEWORK]\Protos\GreetGrpc.cs :
содержит созданные клиентские классы.
Дополнительные сведения о ресурсах C#, автоматически создаваемых
Grpc.Tools , см. в разделе Службы gRPC и C#: создаваемые ресурсы C#.
Добавьте в файл Program.cs клиента gRPC следующий код.
C#
using System.Threading.Tasks;
using Grpc.Net.Client;
using GrpcGreeterClient;
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
В выделенном коде выше замените номер порта localhost 7042 на номер
порта HTTPS , указанный в файле Properties/launchSettings.json проекта
службы GrpcGreeter .
файл Program.cs содержит точку входа и логику для клиента gRPC.
Клиент Greeter создается следующим образом:
Создание экземпляра GrpcChannel со сведениями для создания подключения к
службе gRPC.
Использование GrpcChannel для создания клиента Greeter:
C#
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
Клиент Greeter вызывает асинхронный метод SayHello . Отображается результат
вызова SayHello :
C#
// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
Тестирование клиента gRPC с помощью
службы Greeter gRPC
Visual Studio
В службе Greeter нажмите Ctrl+F5 для запуска сервера без отладчика.
В проекте GrpcGreeterClient нажмите Ctrl+F5 для запуска клиента без
отладчика.
Клиент отправляет приветствие в службу с сообщением, в котором содержится его
имя GreeterClient. Служба отправляет сообщение "Hello GreeterClient" в качестве
ответа. Ответ "Hello GreeterClient" отображается в командной строке:
Консоль
Greeting: Hello GreeterClient
Press any key to exit...
Служба gRPC записывает сведения об успешном вызове в журналы, что
отображается в командной строке:
Консоль
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:<port>
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path:
C:\GH\aspnet\docs\4\Docs\aspnetcore\tutorials\grpc\grpcstart\sample\GrpcGreeter
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/2 POST https://localhost:
<port>/Greet.Greeter/SayHello application/grpc
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'gRPC - /Greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 78.32260000000001ms 200 application/grpc
Обновите файл appsettings.Development.json , добавив следующие строки:
"Microsoft.AspNetCore.Hosting": "Information",
"Microsoft.AspNetCore.Routing.EndpointMiddleware": "Information"
7 Примечание
Чтобы применить код, описанный в этой статье, требуется сертификат
разработки HTTPS ASP.NET Core для защиты службы gRPC. Если .NET gRPC
возвращает сообщение The remote certificate is invalid according to the
validation procedure. или The SSL connection could not be established. , это
значит, что сертификат разработки не является доверенным. Чтобы исправить
эту проблему, см. Вызов службы gRPC с использованием ненадежного или
недействительного сертификата.
2 Предупреждение
ASP.NET Core gRPC предусматривает дополнительные требования в
отношении использования со Службой приложений Azure или службами IIS.
Дополнительные сведения о том, где можно использовать gRPC, см. в статье
Использование gRPC на поддерживаемых платформах .NET.
Следующие шаги
Просмотрите или скачайте готовый пример кода для этого учебника
(инструкции по скачиванию).
Общие сведения о gRPC на .NET
Службы gRPC на языке C#
Перенос gRPC с C-core на gRPC для .NET
Использование Razor Pages с Entity
Framework Core в ASP.NET Core:
руководство 1 из 8
Статья • 28.01.2023 • Чтение занимает 52 мин
Авторы: Том Дайкстра
П. Смит
(Tom Dykstra), Джереми Ликнесс
(Jeremy Likness) и Йон
(Jon P Smith)
Это первое руководство из серии, посвященной использованию Entity Framework
(EF) Core в приложении ASP.NET Core Razor Pages. В учебниках создается веб-сайт
для вымышленного университета Contoso. На сайте предусмотрены различные
функции, в том числе прием учащихся, создание курсов и назначение
преподавателей. В этом руководстве используется подход Code First. См. сведения
о работе с этим руководством при использовании подхода Database First в этой
проблеме GitHub .
Скачайте или ознакомьтесь с готовым приложением. Инструкции по скачиванию.
Предварительные требования
Если у вас нет опыта работы с Razor Pages, ознакомьтесь с серией руководств
Начало работы с Razor Pages, прежде чем приступать к изучению этого
руководства.
Visual Studio
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Ядра СУБД
В инструкциях для Visual Studio используется SQL Server LocalDB, версия
SQL Server Express, которая работает только в Windows.
Устранение неполадок
Если вы столкнулись с проблемой, которую не можете решить, сравните свой код с
кодом готового проекта . Хорошим способом получения справки является
публикация вопроса в StackOverflow.com с помощью тега ASP.NET Core
тегаEF Core
или
.
Пример приложения
Приложение, создаваемое в этих руководствах, является простым веб-сайтом
университета. Пользователи приложения могут просматривать и обновлять
сведения об учащихся, курсах и преподавателях. Здесь приведено несколько
экранов, создаваемых в руководстве.
Стиль пользовательского интерфейса для этого сайта основан на встроенных
шаблонах проектов. В руководстве основное внимание уделяется использованию
EF Core с ASP.NET Core, а не настройке пользовательского интерфейса.
Необязательно: сборка примера для скачивания
Это необязательный шаг. Создание готового приложения рекомендуется в случае,
если возникли проблемы, которые не удается решить. Если вы столкнулись с
проблемой, которую не можете решить, сравните свой код с кодом готового
проекта . Указания по скачиванию.
Visual Studio
Выберите ContosoUniversity.csproj , чтобы открыть проект.
Выполните построение проекта.
В консоли диспетчера пакетов (PMC) выполните следующую команду:
PowerShell
Update-Database
Запустите проект, чтобы заполнить базу данных.
Создание проекта веб-приложения
Visual Studio
1. Запустите Visual Studio 2022 и нажмите Создать проект.
2. В диалоговом окне Создать проект выберите Веб-приложение
ASP.NET Core и нажмите Далее.
3. В диалоговом окне Настроить новый проект введите ContosoUniversity в
поле Имя проекта. Важно присвоить проекту имя ContosoUniversity,
включая сопоставление регистра букв, чтобы пространство имени
соответствовало при копировании и вставке примера кода.
4. Выберите Далее.
5. В диалоговом окне Дополнительные сведения выберите .NET 6.0
(долгосрочная поддержка) и щелкните Создать.
Настройка стиля сайта
Скопируйте и вставьте в файл Pages/Shared/_Layout.cshtml следующий код:
CSHTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"
/>
<link rel="stylesheet" href="~/ContosoUniversity.styles.css" asp-appendversion="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbarlight bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asppage="/Index">Contoso University</a>
<button class="navbar-toggler" type="button" data-bstoggle="collapse" data-bs-target=".navbar-collapse" ariacontrols="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asppage="/About">About</a>
</li>
<li class="nav-item">
<a class="nav-link
page="/Students/Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link
page="/Courses/Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link
page="/Instructors/Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link
page="/Departments/Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
text-dark" asp-area="" asp-
text-dark" asp-area="" asp-
text-dark" asp-area="" asp-
text-dark" asp-area="" asp-
<footer class="border-top footer text-muted">
<div class="container">
© 2021 - Contoso University - <a asp-area="" asppage="/Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Файл макета задает заголовок, нижний колонтитул и меню для сайта. Приведенный
выше код вносит следующие изменения:
Заменяет все вхождения "ContosoUniversity" на "Contoso University". Таких
элементов будет три.
Пункты меню Home и Privacy будут удалены.
Добавляются пункты меню About (Сведения), Students (Учащиеся), Courses
(Курсы), Instructors (Преподаватели) и Departments (Кафедры).
Замените все содержимое файла Pages/Index.cshtml следующим кодом:
CSHTML
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="row mb-auto">
<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 mb-4 ">
<p class="card-text">
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core Razor Pages web app.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 d-flex flex-column position-static">
<p class="card-text mb-auto">
You can build the application by following the steps in
a series of tutorials.
</p>
<p>
@*
<a
href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro"
class="stretched-link">See the tutorial</a>
*@
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="row no-gutters border mb-4">
<div class="col p-4 d-flex flex-column">
<p class="card-text mb-auto">
You can download the completed project from GitHub.
</p>
<p>
@*
<a
href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef
-rp/intro/samples" class="stretched-link">See project source code</a>
*@
</p>
</div>
</div>
</div>
</div>
Приведенный выше код заменяет текст об ASP.NET Core текстом об этом
приложении.
Запустите приложение, чтобы проверить правильность отображения домашней
страницы.
Модель данных
В следующих разделах создается модель данных.
Учащийся может зарегистрироваться в любом количестве курсов, а в отдельном
курсе может быть зарегистрировано любое количество учащихся.
Сущность Student
Создайте папку Models (Модели) в папке проекта.
Создайте Models/Student.cs , используя следующий код:
C#
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Свойство ID используется в качестве столбца первичного ключа в таблице базы
данных, соответствующей этому классу. По умолчанию интерпретирует свойство с
EF Core именем ID или classnameID как первичный ключ. Поэтому альтернативным
автоматически распознаваемым именем для первичного ключа класса Student
является StudentID . Дополнительные сведения см. в разделе EF Core Ключи.
Свойство Enrollments является свойством навигации. Свойства навигации
содержат другие сущности, связанные с этой сущностью. В этом случае свойство
Enrollments сущности Student содержит все сущности Enrollment , которые связаны
с этим учащимся. Например, если строка Student (Учащийся) в базе данных имеет
две связанные строки Enrollment (Регистрация), свойство навигации Enrollments
содержит две эти сущности Enrollment.
В базе данных строка Enrollment связана со строкой Student, если ее столбец
StudentID содержит идентификатор учащегося. Например, предположим, что
строка Student содержит идентификатор 1. Связанные строки Enrollment будут
содержать значение StudentID (идентификатор учащегося), равное 1. StudentID —
это внешний ключ в таблице Enrollment.
Свойство Enrollments определено как ICollection<Enrollment> , так как может быть
несколько связанных сущностей Enrollment. Можно использовать и другие типы
коллекций, например List<Enrollment> или HashSet<Enrollment> . При
ICollection<Enrollment> использовании по EF Core умолчанию создает
HashSet<Enrollment> коллекцию.
Сущность Enrollment
Создайте Models/Enrollment.cs , используя следующий код:
C#
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
Свойство EnrollmentID является первичным ключом. В этой сущности используется
шаблон classnameID вместо ID . Для рабочей модели данных многие разработчики
выбирают один шаблон и используют только его. В этом учебнике используются
оба шаблона, чтобы проиллюстрировать их работу. Использование ID без
classname упрощает внесение некоторых изменений в модель данных.
Свойство Grade имеет тип enum . Знак вопроса после объявления типа Grade
указывает, что свойство Grade допускает значение NULL. Оценка со значением null
отличается от нулевой оценки тем, что при таком значении оценка еще не
известна или не назначена.
Свойство StudentID представляет собой внешний ключ. Ему соответствует свойство
навигации Student . Сущность Enrollment связана с одной сущностью Student ,
поэтому свойство содержит отдельную сущность Student .
Свойство CourseID представляет собой внешний ключ. Ему соответствует свойство
навигации Course . Сущность Enrollment связана с одной сущностью Course .
EF Core интерпретирует свойство как внешний ключ, если оно называется
<navigation property name><primary key property name> . Например, StudentID
является внешним ключом для свойства навигации Student , так как сущность
Student имеет первичный ключ ID . Свойства внешнего ключа также могут
называться <primary key property name> . Например, CourseID , так как сущность
Course имеет первичный ключ CourseID .
Сущность Course
Создайте Models/Course.cs , используя следующий код:
C#
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Свойство Enrollments является свойством навигации. Сущность Course может быть
связана с любым числом сущностей Enrollment .
Атрибут DatabaseGenerated позволяет приложению указать первичный ключ, а не
использовать созданный базой данных.
Построение приложения. Компилятор создает несколько предупреждений о том,
как обрабатываются значения null . Дополнительные сведения см. в сведениях об
этой проблеме GitHub , а также в статьях Ссылочные типы, допускающие
значение NULL и Руководство по более четкому выражению проектного замысла с
помощью ссылочных типов, допускающих и не допускающих значение NULL.
Чтобы исключить предупреждения из ссылочных типов, допускающих значения
NULL, удалите из файла ContosoUniversity.csproj следующую строку:
XML
<Nullable>enable</Nullable>
В настоящее время подсистема формирования шаблонов не поддерживает
ссылочные типы, допускающие значения NULL, поэтому модели, используемые в
формировании шаблонов, также не поддерживают их.
Удалите заметку о ссылочном типе ? , допускающем значение NULL, из public
string? RequestId { get; set; } в файле Pages/Error.cshtml.cs , чтобы сборка
проекта выполнялась без предупреждений компилятора.
Формирование шаблона для страниц
Student
В этом разделе для создания указанных ниже компонентов используется средство
формирования шаблонов ASP.NET Core.
Класс EF Core DbContext . Контекст —это основной класс, который
координирует функциональные возможности Entity Framework для
определенной модели данных. Он является производным от класса
Microsoft.EntityFrameworkCore.DbContext.
Razor Pages с поддержкой операций создания, чтения, обновления и удаления
(CRUD) для сущности Student .
Visual Studio
Создайте папку Pages/Students.
В обозревателе решений щелкните правой кнопкой мыши папку
Pages/Students и выберите пункты Добавить>Создать шаблонный
элемент.
В диалоговом окне Добавление нового элемента шаблона выполните
указанные ниже действия.
На вкладке слева выберите Установленные > Общие >Razor Pages
Выберите Razor Pages на основе Entity Framework (CRUD)>Добавить.
В диалоговом окне Добавление Razor Pages на основе Entity Framework
(CRUD) сделайте следующее:
В раскрывающемся списке Класс модели выберите Student
(ContosoUniversity.Models) .
В строке Класс контекста данных щелкните знак плюса (+).
Измените имя контекста данных так, чтобы оно заканчивалось на
SchoolContext , а не на ContosoUniversityContext . Новое имя
контекста: ContosoUniversity.Data.SchoolContext .
Выберите Добавить, чтобы завершить добавление класса контекста
данных.
Выберите Добавить, чтобы закрыть диалоговое окно Добавление
Razor Pages.
Следующие пакеты устанавливаются автоматически:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
Если предыдущий шаг завершился сбоем, выполните сборку проекта и повторите
шаг формирования шаблона.
В процессе формирования шаблона выполняются следующие действия:
создаются страницы Razor в папке Pages/Students:
Create.cshtml и Create.cshtml.cs
Delete.cshtml и Delete.cshtml.cs
Details.cshtml и Details.cshtml.cs
Edit.cshtml и Edit.cshtml.cs
Index.cshtml и Index.cshtml.cs
Создает Data/SchoolContext.cs .
добавляется контекст для внедрения зависимостей в файле Program.cs ;
добавляет строку подключения к базе данных в файл appsettings.json .
Строка подключения к базе данных
Средство формирования шаблонов создает строку подключения в файле
appsettings.json .
Visual Studio
Строка подключения указывает базу данных SQL Server LocalDB.
JSON
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=
(localdb)\\mssqllocaldb;Database=SchoolContext0e9;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
LocalDB — это упрощенная версия ядра СУБД SQL Server Express,
предназначенная для разработки приложений и не ориентированная на
использование в рабочей среде. По умолчанию LocalDB создает файлы MDF в
каталоге C:/Users/<user> .
Обновление класса контекста базы данных
Основным классом, координирующий функциональные EF Core возможности
данной модели данных, является класс контекста базы данных. Контекст является
производным от Microsoft.EntityFrameworkCore.DbContext. Контекст указывает
сущности, которые включаются в модель данных. В этом проекте соответствующий
класс называется SchoolContext .
Обновите Data/SchoolContext.cs , включив в него следующий код.
C#
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext (DbContextOptions<SchoolContext> options)
: base(options)
{
}
public DbSet<Student> Students { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Course> Courses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}
Приведенный выше код изменяет форму слова DbSet<Student> Student на
множественную: DbSet<Student> Students . Чтобы код Razor Pages соответствовал
новому имени DBSet , произведите глобальное изменение _context.Student. на
_context.Students. .
Всего это имя встречается 8 раз.
Так как набор сущностей содержит несколько сущностей, многие разработчики
предпочитают имена свойств DBSet во множественном числе.
Выделенный код:
Создает свойство DbSet<TEntity> для каждого набора сущностей. В EF Core
терминологии:
Набор сущностей обычно соответствует таблице базы данных.
Сущность соответствует строке в таблице.
Вызывает OnModelCreating. OnModelCreating :
вызывается, когда контекст SchoolContext был инициализирован, но
прежде чем модель была заблокирована и использована для
инициализации контекста;
является обязательным, так как далее в руководстве сущность Student
будет содержать ссылки на другие сущности.
Мы планируем исправить эту проблему
в будущем выпуске.
Program.cs
ASP.NET Core поддерживает внедрение зависимостей. С помощью внедрения
зависимостей службы, например SchoolContext , регистрируются во время запуска
приложения. Затем компоненты, которые используют эти службы, например Razor
Pages, обращаются к ним через параметры конструктора. Код конструктора,
который получает экземпляр контекста базы данных, приведен далее в этом
руководстве.
Средство формирования шаблонов автоматически зарегистрировало класс
контекста в контейнере внедрения зависимостей.
Visual Studio
Следующие выделенные строки были добавлены средством формирования
шаблонов:
C#
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));
Имя строки подключения передается в контекст путем вызова метода для объекта
DbContextOptions. При локальной разработке система конфигурации ASP.NET Core
считывает строку подключения из файла appsettings.json или
appsettings.Development.json .
Добавление фильтра исключений базы данных
Добавьте AddDatabaseDeveloperPageExceptionFilter и UseMigrationsEndPoint, как
показано в следующем коде:
Visual Studio
C#
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
Добавьте пакет NuGet Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore .
Введите в консоли диспетчера пакетов следующие команды, чтобы добавить
пакет NuGet:
PowerShell
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Пакет NuGet Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore предоставляет
ПО промежуточного слоя ASP.NET Core для страниц ошибок Entity Framework Core.
Это ПО промежуточного слоя помогает обнаруживать и диагностировать ошибки с
помощью миграций Entity Framework Core.
AddDatabaseDeveloperPageExceptionFilter предоставляет полезные сведения об
ошибках EF в среде разработки для их устранения.
Создание базы данных
Обновите файл Program.cs , чтобы создать базу данных, если она не существует.
Visual Studio
C#
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolCo
ntext")));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
// DbInitializer.Initialize(context);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Метод EnsureCreated не выполняет никаких действий, если база данных для
контекста существует. Если база данных не существует, она создается вместе со
схемой. EnsureCreated обеспечивает описанный ниже рабочий процесс для
обработки изменений модели данных.
База данных удаляется. Все существующие данные теряются.
Модель данных изменяется. Например, добавляется поле EmailAddress .
Запустите приложение.
Метод EnsureCreated создает базу данных с новой схемой.
Этот рабочий процесс хорошо подходит для ранних стадий разработки, когда
схема часто меняется, если данные сохранять не требуется. Однако если данные,
введенные в базу данных, необходимо сохранять, ситуация будет иной. В таком
случае используйте перенос.
Далее в этой серии учебников вы удалите базу данных, созданную методом
EnsureCreated , и используете вместо этого перенос. Созданную методом
EnsureCreated базу данных нельзя обновить, используя перенос.
Тестирование приложения
Запустите приложение.
Щелкните ссылку Students и выберите Создать.
Протестируйте ссылки Edit, Details и Delete.
Заполнение базы данных
Метод EnsureCreated создает пустую базу данных. В этом разделе добавляется код,
который заполняет базу данных тестовыми данными.
Создайте Data/DbInitializer.cs , используя следующий код:
C#
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return;
// DB has been seeded
}
var students = new Student[]
{
new
Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.P
arse("2019-09-01")},
new
Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Pa
rse("2017-09-01")},
new
Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse
("2018-09-01")},
new
Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Pa
rse("2017-09-01")},
new
Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017
-09-01")},
new
Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Pars
e("2016-09-01")},
new
Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse
("2018-09-01")},
new
Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Pars
e("2019-09-01")}
};
context.Students.AddRange(students);
context.SaveChanges();
var courses = new Course[]
{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
context.Courses.AddRange(courses);
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
context.Enrollments.AddRange(enrollments);
context.SaveChanges();
}
}
}
Этот код проверяет наличие учащихся в базе данных. Если учащихся нет, в базу
данных добавляются тестовые данные. Для повышения производительности
тестовые данные создаются массивами, а не коллекциями List<T> .
В Program.cs удалите // из строки DbInitializer.Initialize :
C#
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<SchoolContext>();
context.Database.EnsureCreated();
DbInitializer.Initialize(context);
}
Visual Studio
Завершите работу приложения, если оно запущено, и выполните
следующую команду в консоли диспетчера пакетов (PMC):
PowerShell
Drop-Database -Confirm
Выберите Y , чтобы удалить базу данных.
Перезапустите приложение.
Выберите страницу учащихся, чтобы увидеть заполненные данные.
Просмотр базы данных
Visual Studio
Откройте обозреватель объектов SQL Server (SSOX) из меню Вид в Visual
Studio.
В SSOX щелкните (localdb)\MSSQLLocalDB > Базы данных >
SchoolContext-{GUID}. Имя базы данных создается на основе имени
контекста, которое вы указали ранее, а также включает дефис и GUID.
Разверните узел Таблицы.
Щелкните правой кнопкой мыши таблицу Student (Учащийся) и выберите
пункт Просмотр данных, чтобы просмотреть созданные столбцы и
строки, вставленные в таблицу.
Щелкните правой кнопкой мыши таблицу Student (Учащийся) и выберите
пункт Просмотреть код, чтобы увидеть, как модель Student соотносится
со схемой таблицы Student .
Асинхронные методы EF в веб-приложениях
ASP.NET Core
Асинхронное программирование — это режим по умолчанию для ASP.NET Core и
EF Core.
Веб-сервер имеет ограниченное число потоков, поэтому при высокой загрузке
могут использоваться все доступные потоки. В таких случаях сервер не может
обрабатывать новые запросы до тех пор, пока не будут высвобождены потоки. В
синхронном коде многие потоки могут быть заняты, не выполняя при этом какиелибо операции и ожидая завершения ввода-вывода. В асинхронном коде в то
время, когда процесс ожидает завершения ввода-вывода, его поток
высвобождается и может использоваться сервером для обработки других
запросов. Таким образом, асинхронный код позволяет более эффективно
использовать ресурсы сервера, который может обрабатывать больше трафика без
задержек.
Во время выполнения асинхронный код использует немного больше служебных
ресурсов. Однако при низком объеме трафика этим можно пренебречь. Тем не
менее в случае большого объема трафика это дает существенный выигрыш в
производительности.
В следующем коде для асинхронного выполнения используются ключевое слово
async, возвращаемое значение Task , ключевое слово await и метод ToListAsync .
C#
public async Task OnGetAsync()
{
Students = await _context.Students.ToListAsync();
}
Ключевое слово async указывает компилятору:
создавать обратные вызовы для частей тела метода;
создавать возвращаемый объект Task.
Тип возвращаемого значения Task представляет текущую операцию.
Ключевое слово await предписывает компилятору разделить метод на две
части. Первая часть завершается операцией, которая запускается в
асинхронном режиме. Вторая часть помещается в метод обратного вызова,
который вызывается при завершении операции.
ToListAsync является асинхронной версией метода расширения ToList .
Некоторые моменты, которые следует учитывать при написании асинхронного
кода, использующего EF Core:
Асинхронно выполняются только те инструкции, в результате которых в базу
данных отправляются запросы или команды. К ним относятся ToListAsync ,
SingleOrDefaultAsync , FirstOrDefaultAsync и SaveChangesAsync . В их число не
входят операторы, которые просто изменяют IQueryable , такие как var
students = context.Students.Where(s => s.LastName == "Davolio") .
Контекст EF Core не является потокобезопасным: не пытайтесь выполнять
несколько операций параллельно.
Чтобы воспользоваться преимуществами производительности асинхронного
кода, убедитесь, что пакеты библиотеки (например, для разбиения по
страницам) используют асинхронные методы, которые вызывают EF Core
методы, отправляющие запросы в базу данных.
Дополнительные сведения об асинхронном программировании см. в разделах
Обзор асинхронной модели и Асинхронное программирование с использованием
ключевых слов Async и Await.
2 Предупреждение
Асинхронная реализация Майкрософт. Data.SqlClient
известные проблемы (No 593
, No 601
имеет некоторые
и другие). Если возникают
непредвиденные проблемы с производительностью, попробуйте использовать
выполнение команд синхронизации, особенно при работе с большим текстом
или двоичными значениями.
Вопросы производительности
Как правило, веб-страница не должна загружать произвольное число строк. Запрос
должен использовать разбиение на страницы или применять ограничение.
Например, предыдущий запрос может использовать Take для ограничения
количества возвращаемых строк:
C#
public async Task OnGetAsync()
{
Student = await _context.Students.Take(10).ToListAsync();
}
Если в процессе перечисления большой таблицы в представлении возникло
исключение базы данных, может быть возвращен ответ HTTP 200 с сообщением о
частично сформированных данных.
Разбиение на страницы рассматривается далее в этом руководстве.
Дополнительные сведения см. в разделе Особенности производительности (EF).
Дальнейшие действия
Использование SQLite для среды разработки и SQL Server для рабочей среды
Следующий учебник
Часть 2. Razor Страницы с EF Core
ASP.NET Core — CRUD
Статья • 28.01.2023 • Чтение занимает 28 мин
Авторы: Том Дайкстра
П. Смит
(Tom Dykstra), Джереми Ликнесс
(Jeremy Likness) и Йон
(Jon P Smith)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
В этом учебнике описывается проверка и настройка шаблонного кода операций
CRUD (создание, чтение, обновление и удаление).
Репозиторий отсутствует.
Некоторые разработчики используют уровень служб или шаблон репозитория,
чтобы создать уровень абстракции между пользовательским интерфейсом (Razor
Pages) и уровнем доступа к данным. В данном учебнике этого не делается. Чтобы
свести к минимуму сложность и сосредоточиться на EF Coreучебнике, EF Core код
добавляется непосредственно в классы модели страниц.
Обновление страницы Details (сведения)
Шаблонный код для страниц учащихся не включает в себя сведения о регистрации.
В этом разделе показано, как регистрации добавляются на страницу Details .
Считывание сведений о регистрации
Чтобы отобразить сведения о регистрации учащегося на странице, эти сведения
необходимо считать. Шаблонный код в файле Pages/Students/Details.cshtml.cs
считывает только сведения Student , но не сведения Enrollment :
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
Замените метод OnGetAsync приведенным ниже кодом для считывания сведений о
регистрации для выбранного учащегося. Изменения выделены.
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
return Page();
}
Методы Include и ThenInclude инструктируют контекст для загрузки свойства
навигации Student.Enrollments , а также свойства навигации Enrollment.Course в
пределах каждой регистрации. Эти методы более подробно рассматриваются в
руководстве, посвященном чтению связанных данных.
Метод AsNoTracking повышает производительность в тех сценариях, где
возвращаемые сущности не обновляются в текущем контексте. AsNoTracking
рассматривается позднее в этом учебнике.
Отображение регистраций
Замените код в файле Pages/Students/Details.cshtml приведенным ниже кодом для
отображения списка регистраций. Изменения выделены.
CSHTML
@page
@model ContosoUniversity.Pages.Students.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Student</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.Enrollments)
</dt>
<dd class="col-sm-10">
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Student.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Student.ID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
Приведенный выше код циклически обрабатывает сущности в свойстве навигации
Enrollments . Для каждой регистрации он отображает название курса и оценку.
Название курса извлекается из сущности Course , которая хранится в свойстве
навигации Course сущности Enrollments.
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните ссылку
Details (Сведения) для учащегося. Отобразится список курсов и оценок для
выбранного учащегося.
Способы чтения одной сущности
В созданном коде для чтения одной сущности используется метод
FirstOrDefaultAsync. Этот метод возвращает значение NULL, если ничего не
найдено. В противном случае возвращается первая найденная строка,
удовлетворяющая условиям фильтра запросов. FirstOrDefaultAsync обычно
предпочтительнее, чем следующие варианты:
SingleOrDefaultAsync — вызывает исключение при наличии нескольких
сущностей, соответствующих фильтру запросов. Для определения того, может
ли запрос вернуть более одной строки, SingleOrDefaultAsync пытается
получить несколько строк. Это дополнительное действие не требуется, если
запрос может вернуть только одну сущность, как при поиске по уникальному
ключу.
FindAsync — находит сущность с первичным ключом. Если сущность с
первичным ключом отслеживается контекстом, она возвращается без запроса
к базе данных. Этот метод оптимизирован для поиска одной сущности, однако
вызвать Include с FindAsync невозможно. Поэтому если требуются связанные
данные, лучше использовать FirstOrDefaultAsync .
Данные маршрута или строка запроса
URL-адрес страницы сведений — https://localhost:<port>/Students/Details?id=1 .
Значение первичного ключа сущности содержится в строке запроса. Некоторые
разработчики предпочитают передавать значение ключа в данных маршрута:
https://localhost:<port>/Students/Details/1 . Дополнительные сведения см. в
разделе Обновление созданного кода.
Обновление страницы Create
Шаблонный код OnPostAsync для страницы создания уязвим к чрезмерной
передаче данных. Замените метод OnPostAsync в файле
Pages/Students/Create.cshtml.cs следующим кодом:
C#
public async Task<IActionResult> OnPostAsync()
{
var emptyStudent = new Student();
if (await TryUpdateModelAsync<Student>(
emptyStudent,
"student",
// Prefix for form value.
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
_context.Students.Add(emptyStudent);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
TryUpdateModelAsync
В приведенном выше коде создается объект Student, после чего опубликованные
поля формы используются для обновления свойств этого объекта. Метод
TryUpdateModelAsync выполняет указанные ниже действия.
Использует опубликованные значения формы из свойства PageContext в
PageModel.
Обновляет только перечисленные свойства ( s => s.FirstMidName, s =>
s.LastName, s => s.EnrollmentDate ).
Ищет поля формы с префиксом "student". Например, Student.FirstMidName .
Задается без учета регистра символов.
Использует систему привязки модели для преобразования значений формы
из строк в типы модели Student . Например, EnrollmentDate преобразуется в
DateTime .
Запустите приложение и создайте сущность учащегося для тестирования страницы
создания.
Чрезмерная передача данных
В целях повышения безопасности рекомендуется использовать TryUpdateModel для
обновления полей на основе отправленных значений, поскольку в этом случае
исключается чрезмерная передача данных. Например, сущность Student включает
свойство Secret , которое веб-страница не должна обновлять или добавлять:
C#
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
Даже если приложение не имеет поля Secret на странице создания или
обновления Razor, злоумышленник может установить значение Secret
посредством чрезмерной передачи данных. Злоумышленник может использовать
такие средства, как Fiddler, или собственный код JavaScript для отправки значения
формы Secret . В исходном коде не ограничиваются поля, которые используются
при создании экземпляра Student связывателем модели.
Какое бы значение ни задал злоумышленник для поля формы Secret , оно будет
обновлено в базе данных. На следующем рисунке показано средство Fiddler, с
помощью которого в отправленные значения формы добавляется поле Secret со
значением "OverPost".
Значение "OverPost" успешно добавлено в свойство Secret вставленной строки.
Это происходит несмотря на то, что разработчик приложения не планировал, что
свойство Secret будет устанавливаться на странице создания.
Модель представления
Модели представления реализуют альтернативный подход к защите от чрезмерной
передачи данных.
Модель приложения часто называют моделью домена. Модель предметной
области обычно содержит все свойства, необходимые для соответствующей
сущности в базе данных. Модель представления содержит только те свойства,
которые необходимы для страницы пользовательского интерфейса, например
страницы Create.
Помимо модели представления в некоторых приложениях используется модель
привязки или модель ввода для передачи данных между классом модели страницы
Razor Pages и браузером.
Рассмотрим следующую модель представления StudentVM :
C#
public class StudentVM
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
}
В следующем коде модель представления используется StudentVM для создания
нового учащегося:
C#
[BindProperty]
public StudentVM StudentVM { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var entry = _context.Add(new Student());
entry.CurrentValues.SetValues(StudentVM);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
Метод SetValues устанавливает значения этого объекта, считывая значения из
другого объекта PropertyValues. SetValues использует сопоставление имен свойств.
Тип модели представления:
не обязательно должен быть связан с типом модели;
должен иметь соответствующие свойства.
При использовании StudentVM требуется, чтобы страница Create использовала
StudentVM , а не Student :
CSHTML
@page
@model CreateVMModel
@{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Student</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="StudentVM.LastName" class="control-label">
</label>
<input asp-for="StudentVM.LastName" class="form-control" />
<span asp-validation-for="StudentVM.LastName" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="StudentVM.FirstMidName" class="controllabel"></label>
<input asp-for="StudentVM.FirstMidName" class="form-control"
/>
<span asp-validation-for="StudentVM.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StudentVM.EnrollmentDate" class="controllabel"></label>
<input asp-for="StudentVM.EnrollmentDate" class="formcontrol" />
<span asp-validation-for="StudentVM.EnrollmentDate"
class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary"
/>
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Обновление страницы редактирования
В файле Pages/Students/Edit.cshtml.cs замените методы OnGetAsync и OnPostAsync
приведенным ниже кодом.
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students.FindAsync(id);
if (Student == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var studentToUpdate = await _context.Students.FindAsync(id);
if (studentToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"student",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
Изменения в коде аналогичны странице Create за некоторыми исключениями:
FirstOrDefaultAsync заменен на FindAsync; Если включать связанные данные
не требуется, более эффективным будет метод FindAsync .
OnPostAsync имеет параметр id .
Текущий учащийся извлекается из базы данных вместо того, чтобы создавать
нового учащегося.
Запустите приложение и протестируйте его, создав и изменив учащегося.
Состояния сущностей
Контекст базы данных отслеживает синхронизацию сущностей в памяти с
соответствующими им строками в базе данных. Данные отслеживания определяют
поведение при вызове метода SaveChangesAsync. Например, при передаче новой
сущности в метод AddAsync ей присваивается состояние Added. При вызове
метода SaveChangesAsync контекст базы данных выполняет команду SQL INSERT .
Возможны следующие состояния сущности:
Added . Сущность еще не существует в базе данных. Метод SaveChanges
выполняет инструкцию INSERT .
Unchanged . изменения сущности не сохраняются. Сущность находится в этом
состоянии при считывании из базы данных.
Modified . Были изменены значения некоторых или всех свойств сущности.
Метод SaveChanges выполняет инструкцию UPDATE .
Deleted . Сущность отмечена для удаления. Метод SaveChanges выполняет
инструкцию DELETE .
Detached . Сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются
автоматически. После считывания сущности и ее изменения ей автоматически
присваивается состояние Modified . При вызове метода SaveChanges создается
инструкция SQL UPDATE , которая обновляет только измененные свойства.
В веб-приложении объект DbContext , который считывает сущность и отображает ее
данные, ликвидируется после отрисовки страницы. При вызове метода страницы
OnPostAsync выполняется новый веб-запрос с новым экземпляром DbContext . Если
повторно считать сущность в этот новый контекст, будет смоделирована обработка
в классическом приложении.
Обновление страницы удаления
В этом разделе реализуется пользовательское сообщение об ошибке на случай
сбоя при вызове SaveChanges .
Замените код в Pages/Students/Delete.cshtml.cs следующим:
C#
using
using
using
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.Logging;
System;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
private readonly ILogger<DeleteModel> _logger;
public DeleteModel(ContosoUniversity.Data.SchoolContext context,
ILogger<DeleteModel> logger)
{
_context = context;
_logger = logger;
}
[BindProperty]
public Student Student { get; set; }
public string ErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int? id, bool?
saveChangesError = false)
{
if (id == null)
{
return NotFound();
}
Student = await _context.Students
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ErrorMessage = String.Format("Delete {ID} failed. Try
again", id);
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var student = await _context.Students.FindAsync(id);
if (student == null)
{
return NotFound();
}
try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, ErrorMessage);
return RedirectToAction("./Delete",
new { id, saveChangesError = true });
}
}
}
}
Предыдущий код:
Добавляет возможность ведения журнала.
Добавляет необязательный параметр saveChangesError в сигнатуру метода
OnGetAsync . saveChangesError указывает, был ли метод вызван после того, как
произошел сбой при удалении объекта учащегося.
Операция удаления может завершиться сбоем из-за временных проблем с сетью.
Вероятность возникновения временных проблем с сетью выше, когда база данных
размещается в облаке. Параметр saveChangesError имеет значение false при
вызове метода OnGetAsync страницы удаления из пользовательского интерфейса.
Если OnGetAsync вызывается методом OnPostAsync из-за сбоя операции удаления,
параметру saveChangesError присваивается значение true .
Метод OnPostAsync извлекает выбранную сущность и вызывает метод Remove,
чтобы присвоить ей состояние Deleted . При вызове метода SaveChanges создается
инструкция SQL DELETE . В случае сбоя Remove :
Возникает исключение базы данных.
Вызывается метод OnGetAsync страницы Delete с параметром
saveChangesError=true .
Добавьте сообщение об ошибке в Pages/Students/Delete.cshtml :
CSHTML
@page
@model ContosoUniversity.Pages.Students.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Student</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.LastName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.FirstMidName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.FirstMidName)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Student.EnrollmentDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Student.EnrollmentDate)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Student.ID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Запустите приложение и удалите учащегося, чтобы протестировать страницу
удаления.
Следующие шаги
Предыдущий учебник
Следующий учебник
Часть 3. Razor Страницы с EF Core
ASP.NET Core — сортировка,
фильтрация, разбиение на страницы
Статья • 28.01.2023 • Чтение занимает 26 мин
Авторы: Том Дайкстра
П. Смит
(Tom Dykstra), Джереми Ликнесс
(Jeremy Likness) и Йон
(Jon P Smith)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
В этом учебнике вы добавите на страницу учащихся функции сортировки,
фильтрации и разбиения на страницы.
На следующем рисунке показана готовая страница. Заголовки столбцов являются
ссылками, щелкнув которые, можно отсортировать столбец. Щелкайте заголовок
столбца для переключения между сортировкой по возрастанию и убыванию.
Добавление сортировки
Чтобы добавить сортировку, замените код в файле Pages/Students/Index.cshtml.cs
на приведенный ниже.
C#
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public
public
public
public
string
string
string
string
NameSort { get; set; }
DateSort { get; set; }
CurrentFilter { get; set; }
CurrentSort { get; set; }
public IList<Student> Students { get; set; }
public async
{
// using
NameSort
DateSort
Task OnGetAsync(string sortOrder)
System;
= String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
= sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}
Предыдущий код:
Требует добавления using System; .
добавляет свойства, которые будут содержать параметры сортировки;
изменяет имя свойства Student на Students ;
заменяет код в методе OnGetAsync .
Метод OnGetAsync принимает параметр sortOrder из строки запроса в URL-адресе.
URL-адрес и строка запроса формируются вспомогательной функцией тегов
привязки.
Параметр sortOrder имеет значение Name или Date . После параметра sortOrder
может стоять _desc , чтобы указать порядок по убыванию. По умолчанию
используется порядок сортировки по возрастанию.
При запросе страницы "Index" по ссылке Students строка запроса отсутствует.
Учащиеся отображаются по фамилии в порядке возрастания. В операторе default
сортировка по фамилии в порядке возрастания используется по умолчанию
( switch ). Когда пользователь щелкает ссылку заголовка столбца, в строке запроса
указывается соответствующее значение sortOrder .
Для формирования гиперссылок в заголовках столбцов страница Razor использует
NameSort и DateSort с соответствующими значениями строки запроса:
C#
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
В коде используется условный оператор C# ?:. Оператор ?: является тернарным
(принимает три операнда). Первая строка указывает, что, когда sortOrder равен
null или пуст, NameSort имеет значение name_desc . Если sortOrder не является NULL
или пустым, для NameSort задается пустая строка.
Следующие два оператора устанавливают гиперссылки в заголовках столбцов на
странице следующим образом:
Текущий порядок сортировки
Гиперссылка "Last Name"
(Фамилия)
Гиперссылка "Date"
(Дата)
"Last Name" (Фамилия) по
descending
ascending
"Last Name" (Фамилия) по
убыванию
ascending
ascending
"Date" (Дата) по возрастанию
ascending
descending
"Date" (Дата) по убыванию
ascending
ascending
возрастанию
Для указания столбца, по которому выполняется сортировка, этот метод использует
LINQ to Entities. Код инициализирует IQueryable<Student> до оператора switch и
изменяет его в этом операторе:
C#
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
При создании или изменении IQueryable запрос в базу данных не отправляется.
Запрос не выполнится, пока объект IQueryable не будет преобразован в
коллекцию. IQueryable преобразуются в коллекцию путем вызова метода, такого
как ToListAsync . Таким образом, код IQueryable создает одиночный запрос,
который не выполняется до следующего оператора:
C#
Students = await studentsIQ.AsNoTracking().ToListAsync();
OnGetAsync можно расширить на случай большого числа сортируемых столбцов.
Сведения об альтернативном способе программирования этой функциональности
см. в статье Использование динамических запросов LINQ для упрощения кода в
версии этой серии учебников для MVC.
Добавление гиперссылок для заголовков столбцов на
странице индексов учащихся
Замените код в Students/Index.cshtml следующим: Изменения выделены.
CSHTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model =>
model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model =>
model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model =>
model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.ID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Предыдущий код:
Добавляет гиперссылки в заголовки столбцов LastName и EnrollmentDate .
Использует эти сведения в NameSort и DateSort для настройки гиперссылок с
использованием текущих значений порядка сортировки.
Изменяет заголовок страницы с Index (Индекс) на Students (Учащиеся).
Изменяет Model.Student на Model.Students .
Чтобы проверить работу сортировки, сделайте следующее:
Запустите приложение и откройте вкладку Students (Учащиеся).
Щелкните заголовки столбцов.
Добавление фильтрации
Для добавления фильтрации на страницу индекса учащихся:
На страницу Razor добавляется текстовое поле и кнопка отправки. Текстовое
поле предоставляет строку поиска для имени или фамилии.
Страничная модель обновляется для использования значения из текстового
поля.
Обновление метода OnGetAsync
Чтобы добавить фильтрацию, замените код в файле Students/Index.cshtml.cs на
приведенный ниже.
C#
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public
public
public
public
string
string
string
string
NameSort { get; set; }
DateSort { get; set; }
CurrentFilter { get; set; }
CurrentSort { get; set; }
public IList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder, string searchString)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s =>
s.LastName.Contains(searchString)
||
s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}
Предыдущий код:
Добавляет параметр searchString в метод OnGetAsync и сохраняет значение
параметра в свойстве CurrentFilter . Значение строки поиска получается из
текстового поля, добавляемого в следующем разделе.
Добавляет в инструкцию LINQ предложение Where . Это предложение Where
отбирает только учащихся, чье имя или фамилия содержат строку поиска.
Оператор LINQ выполняется, только если задано значение для поиска.
IQueryable и IEnumerable
Код вызывает метод Where объекта IQueryable , при этом фильтр обрабатывается
на сервере. В некоторых случаях приложение может вызывать метод Where как
метод расширения для коллекции в памяти. Например, предположим, что
_context.Students изменения из EF Core DbSet метода репозитория,
возвращающего коллекцию IEnumerable . Обычно результат остается прежним, но
в некоторых случаях он может отличаться.
Например, реализация Contains в .NET Framework по умолчанию выполняет
сравнение с учетом регистра. В SQL Server Contains учет регистра определяется
параметрами сортировки у экземпляра SQL Server. По умолчанию SQL Server не
учитывает регистр. В SQLite по умолчанию регистр учитывается. Можно вызвать
ToUpper , чтобы явно велеть тесту не учитывать регистр.
C#
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`
Приведенный выше код гарантирует, что фильтр не учитывает регистр, даже если
метод Where вызывается для IEnumerable или выполняется в SQLite.
Когда Contains вызывается для коллекции IEnumerable , используется реализация
.NET Core. Когда Contains вызывается для объекта IQueryable , используется
реализация базы данных.
Вызов Contains для IQueryable обычно предпочтительнее по соображениям
производительности. При использовании IQueryable фильтрация выполняется
сервером базы данных. Если сначала создается IEnumerable , все строки должны
возвращаться с сервера базы данных.
Вызов ToUpper снижает производительность. Код ToUpper добавляет функцию в
предложение WHERE TSQL-оператора SELECT. Добавленная функция не позволяет
оптимизатору использовать индекс. Учитывая, что SQL устанавливается без учета
регистра, рекомендуется не использовать вызов ToUpper , когда он не требуется.
Дополнительные сведения см. в статье Использование запроса без учета регистра с
поставщиком SQLite .
Обновление страницы Razor
Замените код в файле Pages/Students/Index.cshtml , чтобы создать кнопку Search
(Поиск).
CSHTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString"
value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model =>
model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model =>
model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model =>
model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.ID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Для добавления кнопки и поля поиска предыдущий код использует
вспомогательную функцию тегов <form> . По умолчанию вспомогательная функция
тегов <form> отправляет данные формы с помощью POST. При этом параметры
передаются в тексте сообщения HTTP, а не в URL-адресе. При использовании HTTP
GET данные формы передаются в виде строк запроса в URL-адресе. Передача
данных со строками запроса позволяет пользователям добавлять URL-адрес в
закладки. Руководства консорциума W3C
рекомендуют использовать GET, когда
действие не приводит к обновлению.
Проверьте работу приложения:
Выберите вкладку Students (Учащиеся) и введите строку поиска. При
использовании SQLite фильтр не учитывает регистр, только если вы
реализовали необязательный код ToUpper , приведенный ранее.
Выберите Search (Поиск).
Обратите внимание, что URL-адрес содержит строку поиска. Пример:
browser-address-bar
https://localhost:5001/Students?SearchString=an
Если страница добавлена в закладки, закладка содержит URL-адрес страницы и
строку запроса SearchString . Формирование строки запроса обеспечивает
method="get" в теге form .
Сейчас при выборе ссылки сортировки заголовка столбца теряется значение
фильтра в поле Search (Поиск). Потерянное значение фильтра исправляется в
следующем разделе.
Добавление разбиения по страницам
В этом разделе создается класс PaginatedList для поддержки разбиения на
страницы. Класс PaginatedList использует операторы Skip и Take для фильтрации
данных на сервере вместо того, чтобы извлекать все строки таблицы. На
следующем рисунке показаны кнопки перелистывания.
Создание класса PaginatedList
В папке проекта создайте файл PaginatedList.cs со следующим кодом:
C#
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
Microsoft.EntityFrameworkCore;
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int
pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;
public bool HasNextPage => PageIndex < TotalPages;
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
В предыдущем коде метод CreateAsync принимает размер и номер страницы и
вызывает соответствующие методы Skip и Take объекта IQueryable . Метод
ToListAsync объекта IQueryable при вызове возвращает список, содержащий
только запрошенную страницу. Для включения и отключения кнопок
перелистывания страниц Previous (Назад) и Next (Далее) используются свойства
HasPreviousPage и HasNextPage .
Метод CreateAsync используется для создания PaginatedList<T> . Конструктор не
позволяет создать объект PaginatedList<T> , так как конструкторы не могут
выполнять асинхронный код.
Добавление размера страницы в конфигурацию
Добавьте PageSize в файл appsettings.json конфигурации:
JSON
{
"PageSize": 3,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Добавление разбиения по страницам в IndexModel
Чтобы добавить разбиение на страницы, замените код в файле
Students/Index.cshtml.cs .
C#
using
using
using
using
using
using
using
using
ContosoUniversity.Data;
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.Configuration;
System;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
private readonly IConfiguration Configuration;
public IndexModel(SchoolContext context, IConfiguration
configuration)
{
_context = context;
Configuration = configuration;
}
public
public
public
public
string
string
string
string
NameSort { get; set; }
DateSort { get; set; }
CurrentFilter { get; set; }
CurrentSort { get; set; }
public PaginatedList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
CurrentFilter = searchString;
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s =>
s.LastName.Contains(searchString)
||
s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s =>
s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
var pageSize = Configuration.GetValue("PageSize", 4);
Students = await PaginatedList<Student>.CreateAsync(
studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
}
}
Предыдущий код:
изменяет тип свойства Students с IList<Student> на PaginatedList<Student> ;
добавляет индекс страницы, текущий порядок sortOrder и фильтр
currentFilter в сигнатуру метода OnGetAsync ;
сохраняет порядок сортировки в свойстве CurrentSort ;
сбрасывает индекс страницы в значение 1 при получении новой строки
поиска;
использует класс PaginatedList для получения сущностей Student.
Задает для pageSize значение 3 из файла конфигурации или 4, если настройка
завершается сбоем.
Все параметры, получаемые методом OnGetAsync , равны NULL, когда:
Страница вызывается по ссылке Students (Учащиеся).
Пользователь не открывал ссылку перелистывания или сортировки.
При выборе ссылки перелистывания переменная индекса страницы содержит
номер страницы для отображения.
Свойство CurrentSort предоставляет странице Razor текущий порядок сортировки.
Текущий порядок сортировки нужно включить в ссылки перелистывания, чтобы
сохранить его при смене страницы.
Свойство CurrentFilter предоставляет странице Razor текущую строку фильтра.
Значение CurrentFilter :
Нужно включить в ссылки для перелистывания, чтобы сохранить параметры
фильтра при смене страницы.
Нужно восстановить в текстовом поле после обновления страницы.
Если строка поиска изменяется во время перелистывания, номер страницы
сбрасывается в значение 1. Номер страницы должен быть сброшен на 1, так как с
новым фильтром может измениться состав отображаемых данных. Если введено
значение для поиска и нажата кнопка Submit (Отправить):
Строка поиска изменяется.
Значение параметра searchString отличается от null.
Метод PaginatedList.CreateAsync преобразует результат запроса учащихся в
отдельную страницу коллекции, поддерживающую разбиение на страницы. Эта
страница с учащимися передается на страницу Razor.
Два вопросительных знака после pageIndex в вызове PaginatedList.CreateAsync
являются оператором объединения с NULL. Оператор объединения с null
определяет значение по умолчанию для типа, допускающего значение null.
Выражение pageIndex ?? 1 возвращает значение свойства pageIndex , если оно есть.
В противном случае возвращается значение 1.
Добавление ссылок для разбиения по страницам
Замените код в Students/Index.cshtml следующим: Изменения выделены:
CSHTML
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString"
value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model =>
model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model =>
model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model =>
model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.ID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
Ссылки в заголовках столбцов передают текущую строку поиска в метод
OnGetAsync с помощью строки запроса:
CSHTML
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
Кнопки перелистывания отображаются вспомогательными функциями тегов:
CSHTML
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
Запустите приложение и перейдите на страницу учащихся.
Чтобы убедиться, что разбиение на страницы работает, нажимайте кнопки
перелистывания при различном порядке сортировки.
Чтобы убедиться, что разбиение на страницы работает корректно вместе с
сортировкой и фильтрацией, введите строку поиска и попробуйте
перелистнуть страницу.
Группирование
В этом разделе показано, как создать страницу общих сведений About , на которой
показано количество учащихся, зарегистрированных на каждую дату. Это
изменение использует группирование и включает следующие шаги:
Создание модели представления для данных, используемых страницей About
(Сведения).
Обновите страницу About для использования модели представления.
Создание модели представления
Создайте папку Models/SchoolViewModels.
Создайте SchoolViewModels/EnrollmentDateGroup.cs , используя следующий код:
C#
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
Создание Razor страницы
Создайте файл Pages/About.cshtml со следующим кодом:
CSHTML
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
Создание модели страницы
Обновите файл Pages/About.cshtml.cs , используя следующий код:
C#
using
using
using
using
using
using
using
using
ContosoUniversity.Models.SchoolViewModels;
ContosoUniversity.Data;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
ContosoUniversity.Models;
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
public AboutModel(SchoolContext context)
{
_context = context;
}
public IList<EnrollmentDateGroup> Students { get; set; }
public async Task OnGetAsync()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
Students = await data.AsNoTracking().ToListAsync();
}
}
}
Запрос LINQ группирует записи из таблицы студентов по дате зачисления,
вычисляет число записей в каждой группе и сохраняет результаты в коллекцию
объектов моделей представления EnrollmentDateGroup .
Запустите приложение и перейдите на страницу "About" (О программе).
Количество зачисленных студентов по дням отображается в таблице.
Дальнейшие действия
В следующем руководстве приложение использует миграции для обновления
модели данных.
Предыдущий учебник
Следующий учебник
Часть 4. Razor Страницы с EF Core
миграцией в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 10 мин
Авторы: Том Дайкстра
Андерсон
(Tom Dykstra), Йон П. Смит
(Jon P Smith) и Рик
(Rick Anderson)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
В этом руководстве описана EF Core функция миграции для управления
изменениями модели данных.
При разработке нового приложения модель данных часто изменяется. При каждом
изменении нарушается синхронизация модели с базой данных. Вы начали работу с
этой серией учебников с настройки платформы Entity Framework для создания базы
данных, если она еще не существует. При каждом изменении модели данных базу
данных необходимо удалять. При следующем запуске приложения вызов
EnsureCreated повторно создает базу данных в соответствии с новой моделью
данных. Затем выполняется класс DbInitializer для заполнения новой базы
данных.
Такой подход к обеспечению синхронизации базы данных с использованием
модели данных хорошо работает, пока приложение не нужно развертывать в
рабочей среде. Приложение, выполняющееся в рабочей среде, обычно содержит
данные. Приложение не может запускаться с тестовой базой данных каждый раз
при внесении изменений (например, при добавлении столбца). Функция EF Core
миграции решает эту проблему, позволяя EF Core обновить схему базы данных
вместо создания новой базы данных.
Вместо удаления и повторного создания базы данных при изменении модели
функция миграций обновляет схему, сохраняя существующие данные.
7 Примечание
Ограничения SQLite
В этом руководстве используется функция миграции Entity Framework Core, где
это возможно. Во время миграции обновляется схема базы данных в
соответствии с изменениями в модели данных. Однако миграции могут
вносить только такие изменения, которые поддерживает ядро СУБД, а
возможности изменения схемы SQLite ограничены. Например, добавление
столбца поддерживается, но удаление столбца не поддерживается. Если
миграция создается для удаления столбца, команда ef migrations add
выполняется успешно, а команда ef database update — нет.
Обходной путь для ограничений SQLite — вручную написать код миграции для
перестроения таблицы в случае изменений. Код для миграции находится в
методах Up и Down и выполняет следующие действия:
Создание новой таблицы.
Копирование данных из старой таблицы в новую.
Удаление старой таблицы.
Переименование новой таблицы.
Написание кода такого рода для конкретной базы данных выходит за рамки
данного учебника. Вместо этого в данном учебнике база данных удаляется и
создается повторно, когда попытка применить миграцию завершается сбоем.
Дополнительные сведения см. в следующих ресурсах:
Ограничения поставщика базы данных SQLite EF Core
Настройка кода миграции
Присвоение начальных значений данных
Инструкция по ALTER TABLE SQLite
Удаление базы данных
Visual Studio
Используйте обозреватель объектов SQL Server, чтобы удалить базу данных,
или выполните следующую команду в консоли диспетчера пакетов (PMC):
PowerShell
Drop-Database
Создание первоначальной миграции
Visual Studio
Выполните следующие команды в PMC:
PowerShell
Add-Migration InitialCreate
Update-Database
Удаление EnsureCreated
Эта серия учебников началась с использования EnsureCreated. Метод EnsureCreated
не создает таблицу журнала миграций и поэтому не может использоваться с
функцией миграций. Он предназначен для тестирования или быстрого создания
прототипов, когда часто требуется удалять и повторно создавать базу данных.
Начиная с этого момента в учебниках будут использоваться миграции.
Удалите Program.cs следующую строку:
C#
context.Database.EnsureCreated();
Запустите приложение и убедитесь в том, что база данных заполняется.
Методы Up и Down
Команда EF Core migrations add сгенерирована кодом для создания базы данных.
Код миграций содержится в файле Migrations\<timestamp>_InitialCreate.cs . Метод
Up класса InitialCreate создает таблицы базы данных, соответствующие наборам
сущностей модели данных. Метод Down удаляет их, как показано в следующем
примере:
C#
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace ContosoUniversity.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true),
Credits = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});
migrationBuilder.CreateTable(
name: "Student",
columns: table => new
{
ID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
LastName = table.Column<string>(nullable: true),
FirstMidName = table.Column<string>(nullable: true),
EnrollmentDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Student", x => x.ID);
});
migrationBuilder.CreateTable(
name: "Enrollment",
columns: table => new
{
EnrollmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
CourseID = table.Column<int>(nullable: false),
StudentID = table.Column<int>(nullable: false),
Grade = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Enrollment", x => x.EnrollmentID);
table.ForeignKey(
name: "FK_Enrollment_Course_CourseID",
column: x => x.CourseID,
principalTable: "Course",
principalColumn: "CourseID",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Enrollment_Student_StudentID",
column: x => x.StudentID,
principalTable: "Student",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Enrollment_CourseID",
table: "Enrollment",
column: "CourseID");
migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Enrollment");
migrationBuilder.DropTable(
name: "Course");
migrationBuilder.DropTable(
name: "Student");
}
}
}
Приведенный выше код предназначен для первоначальной миграции. Код.
был создан командой migrations add InitialCreate ;
выполняется командой database update ;
создает базу данных для модели данных, заданной классом контекста базы
данных.
Параметр имени миграции (в примере это InitialCreate ) используется в качестве
имени файла. В качестве имени миграции можно использовать любое допустимое
имя файла. Рекомендуется выбрать слово или фразу, которые кратко описывают
назначение миграции. Например, миграция, обеспечивающая добавление таблицы
кафедр, может называться "AddDepartmentTable".
Таблица журнала миграции
Используйте SSOX или средство SQLite для просмотра базы данных.
Обратите внимание на добавление таблицы __EFMigrationsHistory . Таблица
__EFMigrationsHistory используется для отслеживания миграций, которые
были применены к базе данных.
Просмотрите данные в таблице __EFMigrationsHistory . Вы увидите одну строку
для первой миграции.
Моментальный снимок модели данных
Миграции создают моментальный снимок текущей модели данных в
Migrations/SchoolContextModelSnapshot.cs . После добавления миграции EF
определяет изменения, сравнивая текущую модель данных с файлом
моментального снимка.
Так как файл моментального снимка отслеживает состояние модели данных,
миграцию нельзя удалить, удалив файл <timestamp>_<migrationname>.cs . Чтобы
отменить последнюю миграцию, используйте команду migrations remove.
migrations remove удаляет миграцию и гарантирует корректный сброс
моментального снимка. Дополнительные сведения см. в разделе dotnet ef
migrations remove.
См. дополнительные сведения в статье Сброс всех миграций.
Применение миграций в рабочей среде
Для рабочих приложений не рекомендуется вызывать Database.Migrate при
запуске приложения. Migrate не следует вызывать из приложения, развернутого в
ферме серверов. Если приложение масштабируется в нескольких экземплярах
сервера, трудно гарантировать, что изменения схемы базы данных не будут
выполняться с нескольких серверов и не будут конфликтовать с обращениями для
чтения или записи.
Миграция базы данных должна выполняться контролируемым способом в рамках
развертывания. Подход к миграции рабочей базы данных включает следующее:
Использование миграций для создания сценариев SQL и их использования в
развертывании.
Выполнение dotnet ef database update из контролируемой среды.
Устранение неполадок
Если приложение использует SQL Server LocalDB и выводит следующее
исключение:
text
SqlException: Cannot open database "ContosoUniversity" requested by the
login.
The login failed.
Login failed for user 'user name'.
решением может быть выполнение dotnet ef database update из командной
строки.
Дополнительные ресурсы
EF Core CLI.
Команды CLI для миграций dotnet EF
Консоль диспетчера пакетов (Visual Studio)
Следующие шаги
В следующем руководстве продолжается построение модели данных путем
добавления свойств сущностей и новых сущностей.
Предыдущий учебник
Следующий учебник
Часть 5. Razor Страницы с EF Core
ASP.NET Core — модель данных
Статья • 28.01.2023 • Чтение занимает 73 мин
Авторы: Том Дайкстра
П. Смит
(Tom Dykstra), Джереми Ликнесс
(Jeremy Likness) и Йон
(Jon P Smith)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
Предыдущие руководства работали с базовой моделью данных, состоящей из трех
сущностей. В этом руководстве:
Добавляются дополнительные сущности и связи.
Настраивается модель данных с помощью указания правил для
форматирования, проверки и сопоставления базы данных.
Готовая модель данных показана на следующем рисунке:
Следующая диаграмма базы данных создана с помощью средства Dataedo :
Чтобы создать диаграмму базы данных с помощью Dataedo, выполните следующие
действия:
Развертывание приложения в Azure
Скачайте и установите Dataedo
на своем компьютере.
Следуйте инструкциям руководства Создание документации для Базы данных
SQL Azure за 5 минут .
На предыдущей диаграмме Dataedo элемент CourseInstructor — это таблица
соединений, созданная Entity Framework. Дополнительные сведения см. в разделе
Многие ко многим.
Сущность Student
Замените код в Models/Student.cs следующим:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than
50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
В приведенном выше коде добавляется свойство FullName и добавляются
следующие атрибуты к существующим свойствам:
[DataType]
[DisplayFormat]
[StringLength]
[Column]
[Required]
[Display]
Вычисляемое свойство FullName
FullName — это вычисляемое свойство, которое возвращает значение, созданное
путем объединения двух других свойств. FullName не может быть задано, поэтому
имеет только метод доступа get. В базе данных не создается столбец FullName .
Атрибут DataType
C#
[DataType(DataType.Date)]
Сейчас для дат зачисления учащихся на всех страницах отображаются время и
дата, хотя важна только дата. Используя атрибуты заметок к данным, вы можете
внести в код одно изменение, позволяющее исправить формат отображения на
каждой странице, где отображаются эти данные.
Атрибут DataType указывает тип данных более точно по сравнению со встроенным
типом базы данных. В этом случае следует отображать отобразить только дату, а не
дату и время. В перечислении DataType представлено множество типов данных,
таких как Date, Time, PhoneNumber, Currency, EmailAddress и других. Атрибут
DataType также обеспечивает автоматическое предоставление функций для
определенных типов в приложении. Пример:
Ссылка mailto: для DataType.EmailAddress создается автоматически.
Средство выбора даты предоставляется для DataType.Date в большинстве
браузеров.
Атрибут DataType создает атрибуты HTML 5 data- . Атрибуты DataType не
предназначены для проверки.
Атрибут DisplayFormat
C#
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =
true)]
DataType.Date не задает формат отображаемой даты. По умолчанию поле даты
отображается с использованием форматов, установленных в CultureInfo сервера.
С помощью атрибута DisplayFormat можно явно указать формат даты: Параметр
ApplyFormatInEditMode указывает, что формат должен применяться к
пользовательскому интерфейсу редактирования. Некоторым полям не следует
использовать ApplyFormatInEditMode . Например, обозначение денежной единицы в
общем случае не должно отображаться в поле редактирования текста.
Атрибут DisplayFormat можно использовать отдельно. Однако чаще всего DataType
рекомендуется применять вместе с атрибутом DisplayFormat . Атрибут DataType
передает семантику данных (в отличие от способа их вывода на экран). Атрибут
DataType дает следующие преимущества, недоступные в DisplayFormat :
Поддержка функций HTML5 в браузере. Например, отображение элемента
управления календарем, соответствующего языковому стандарту обозначения
денежной единицы, ссылок электронной почты, проверки входных данных на
стороне клиента.
По умолчанию формат отображения данных в браузере определяется в
соответствии с установленным языковым стандартом.
Дополнительные сведения см. в документации по вспомогательной функции тегов
<input>.
Атрибут StringLength
C#
[StringLength(50, ErrorMessage = "First name cannot be longer than 50
characters.")]
С помощью атрибутов можно указать правила проверки данных и сообщения об
ошибках проверки. Атрибут StringLength указывает минимальную и максимальную
длину символов, разрешенных в поле данных. Представленный код задает
ограничение длины имен в 50 символов. Пример, в котором задается минимальная
длина строки, приводится далее.
Атрибут StringLength также обеспечивает проверку на стороне клиента и на
стороне сервера. Минимальное значение не оказывает влияния на схему базы
данных.
Атрибут StringLength не запрещает пользователю ввести пробел в качестве имени
пользователя. Атрибут RegularExpression можно использовать для применения
ограничений к входным данным. Например, следующий код требует, чтобы
первый символ был прописной буквой, а остальные символы были буквенными:
C#
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
Visual Studio
В обозревателе объектов SQL Server (SSOX) откройте конструктор таблиц
учащихся, дважды щелкнув таблицу Student (Учащийся).
На предыдущем изображении показана схемы для таблицы Student . Поля
имен имеют тип nvarchar(MAX) . Когда далее в этом учебнике будет создана и
применена миграция, поля имен станут nvarchar(50) из-за атрибутов длины
строки.
Атрибут Column
C#
[Column("FirstName")]
public string FirstMidName { get; set; }
Атрибуты могут управлять, как именно классы и свойства сопоставляются с базой
данных. В модели Student атрибут Column используется для сопоставления имени
свойства FirstMidName с "FirstName" в базе данных.
При создании базы данных имена свойств в модели используются для имен
столбцов (кроме случая, когда используется атрибут Column ). Модель Student
использует FirstMidName для поля имени, так как это поле также может содержать
отчество.
С атрибутом [Column] поле Student.FirstMidName в модели данных сопоставляется
со столбцом FirstName таблицы Student . Добавление атрибута Column изменяет
модель для поддержки SchoolContext . Модель, поддерживающая SchoolContext ,
больше не соответствует базе данных. Это несоответствие будет устранено путем
добавления миграции далее в этом учебнике.
Атрибут Required
C#
[Required]
Атрибут Required делает свойства имен обязательными полями. Атрибут Required
не нужен для типов, не допускающих значения NULL, например для типов
значений (таких как DateTime , int и double ). Типы, которые не могут принимать
значение null, автоматически обрабатываются как обязательные поля.
Для применения MinimumLength нужно использовать атрибут Required с
MinimumLength .
C#
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength и Required разрешают использовать пробелы при проверке.
Используйте атрибут RegularExpression для полного контроля над строкой.
Атрибут Display
C#
[Display(Name = "Last Name")]
Атрибут Display указывает, что заголовки для текстовых полей должны иметь вид
"First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment
Date" (Дата зачисления). По умолчанию в заголовках не использовался пробел для
разделения слов, например "Lastname".
Создание миграции
Запустите приложение и перейдите на страницу Students. Возникает исключение.
Атрибут [Column] приводит к тому, что EF ожидает столбец с именем FirstName , но
имя столбца в базе данных по-прежнему FirstMidName .
Visual Studio
Сообщение об ошибке подобно приведенному ниже.
SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:
SchoolContext
В PMC введите следующие команды для создания миграции и
обновления базы данных:
PowerShell
Add-Migration ColumnFirstName
Update-Database
Первая из этих команд выдает следующее предупреждающее сообщение:
text
An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Это предупреждение вызвано тем, что поля имен теперь ограничены 50
символами. Если имя в базе данных имеет больше 50 символов, символы
с 51-го до последнего будут потеряны.
Откройте таблицу "Student" (Учащийся) в окне SSOX:
До применения миграции столбцы имен имели тип nvarchar(MAX). Теперь
столбцы имен имеют тип nvarchar(50) . Имя столбца изменилось с
FirstMidName на FirstName .
Запустите приложение и перейдите на страницу Students.
Обратите внимание, что значения времени не вводятся и не отображаются
вместе с датами.
Выберите Create New (Создать) и попробуйте ввести имя длиной более
50 символов.
7 Примечание
В следующих разделах сборка приложения на некоторых этапах приводит к
возникновению ошибок компилятора. В инструкциях указано, когда
производить сборку приложения.
Сущность Instructor
Создайте Models/Instructor.cs , используя следующий код:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<Course> Courses { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
На одной строке могут находиться несколько атрибутов. Атрибуты HireDate можно
записать следующим образом:
C#
[DataType(DataType.Date),Display(Name = "Hire
Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
Свойства навигации
Courses и OfficeAssignment — это свойства навигации.
Преподаватель может проводить любое количество курсов, поэтому Courses
определен как коллекция.
C#
public ICollection<Course> Courses { get; set; }
Преподаватель может иметь не более одного кабинета, поэтому свойство
OfficeAssignment содержит одну сущность OfficeAssignment . OfficeAssignment имеет
значение null, если кабинет не назначен.
C#
public OfficeAssignment OfficeAssignment { get; set; }
Сущность OfficeAssignment
Создайте Models/OfficeAssignment.cs , используя следующий код:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
Атрибут Key
Атрибут [Key] используется для идентификации свойства в качестве первичного
ключа (PK), когда имя свойства отличается от classnameID или ID .
Между сущностями Instructor и OfficeAssignment действует связь один к нулю или
к одному. Назначение кабинета существует только в связи с преподавателем,
которому оно назначено. Первичный ключ OfficeAssignment также является
внешним ключом (FK) для сущности Instructor . Связь "один к нулю или один к
одному" возникает, когда PK в одной таблице является как PK, так и FK для другой
таблицы.
EF Core не может автоматически распознать InstructorID как PK OfficeAssignment ,
так как InstructorID не соответствует соглашению об именовании ID или
classnameID. Таким образом, атрибут Key используется для определения
InstructorID в качестве первичного ключа:
C#
[Key]
public int InstructorID { get; set; }
По умолчанию EF Core ключ считается не базой данных, так как столбец
предназначен для идентификации связи. Дополнительные сведения см. в статье о
ключах EF.
Свойство навигации Instructor
Свойство навигации Instructor.OfficeAssignment может иметь значение NULL, так
как строка OfficeAssignment для преподавателя может отсутствовать.
Преподавателю может быть не назначен кабинет.
Свойство навигации OfficeAssignment.Instructor всегда будет иметь сущность
Instructor, так как тип внешнего ключа InstructorID — это тип значения int , не
допускающий значения NULL. Назначение кабинета не может существовать без
преподавателя.
Когда сущность Instructor имеет связанную сущность OfficeAssignment , каждая из
них имеет ссылку на другую в своем свойстве навигации.
Сущность Course
Обновите Models/Course.cs , включив в него следующий код.
C#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<Instructor> Instructors { get; set; }
}
}
Сущность Course имеет свойство внешнего ключа (FK) DepartmentID . DepartmentID
указывает на связанную сущность Department . Сущность Course имеет свойство
навигации Department .
EF Core не требуется свойство внешнего ключа для модели данных, если модель
имеет свойство навигации для связанной сущности. EF Core автоматически создает
FK в базе данных, где бы они ни находились. EF Core создает теневые свойства для
автоматически созданных FK. Однако явное включение внешнего ключа в модель
данных позволяет сделать обновления проще и эффективнее. Например,
рассмотрим модель, где свойство внешнего ключа DepartmentID не включено. При
получении сущности курса для редактирования:
свойство Department имеет значение null , если оно не загружено явно;
для обновления сущности курса нужно сначала получить сущность
Department .
Если свойство внешнего ключа DepartmentID включено в модель данных, получать
сущность Department перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут [DatabaseGenerated(DatabaseGeneratedOption.None)] указывает, что
первичный ключ предоставляется приложением, а не создается базой данных.
C#
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию предполагается, EF Core что значения PK создаются базой данных.
Обычно лучше всего использовать значения, созданные базой данных. Для
сущностей Course пользователь указывает первичный ключ. Например, номер
курса, такой как серия 1000 для кафедры математики и серия 2000 для кафедры
английского языка.
Атрибут DatabaseGenerated также может использоваться для создания значений по
умолчанию. Например, база данных может автоматически создать поле даты для
записи даты, когда строка была создана или изменена. Дополнительные сведения
см. в разделе Созданные свойства.
Свойства внешнего ключа и навигации
Свойства внешнего ключа (FK) и свойства навигации в сущности Course отражают
следующие связи:
Курс назначается одной кафедре, поэтому имеется внешний ключ DepartmentID и
свойство навигации Department .
C#
public int DepartmentID { get; set; }
public Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство
навигации Enrollments является коллекцией:
C#
public ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации
Instructors является коллекцией:
C#
public ICollection<Instructor> Instructors { get; set; }
Сущность Department
Создайте Models/Department.cs , используя следующий код:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Атрибут Column
Ранее атрибут Column использовался, чтобы изменить сопоставление имени
столбца. В коде для сущности Department атрибут Column можно использовать,
чтобы изменить сопоставление типов данных SQL. Столбец Budget определяется с
помощью типа money SQL Server в базе данных:
C#
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется. EF Coreвыбирает соответствующий
SQL Server тип данных на основе типа CLR для свойства. Тип decimal среды CLR
сопоставляется с типом decimal SQL Server. Budget используется для денежных
единиц, хотя для этого лучше подходит тип данных money.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Кафедра может иметь или не иметь администратора.
Администратор всегда является преподавателем. Поэтому свойство
InstructorID включается в качестве внешнего ключа в сущность Instructor .
Свойство навигации называется Administrator , но содержит сущность Instructor :
C#
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Вопросительный знак ? в приведенном выше коде указывает, что свойство
допускает значение NULL.
Кафедра может иметь несколько курсов, поэтому доступно свойство навигации
Courses:
C#
public ICollection<Course> Courses { get; set; }
По соглашению EF Core включает каскадное удаление для не допускающих
значения NULL FK и для связей "многие ко многим". Это поведение по умолчанию
может привести к циклическим правилам каскадного удаления. Такие правила
вызывают исключение при добавлении миграции.
Например, если Department.InstructorID свойство было определено как не
допускаемое значение NULL, EF Core настройте правило каскадного удаления. В
этом случае кафедра будет удалена, если будет удален преподаватель,
назначенный ее администратором. В такой ситуации правило ограничения будет
более целесообразным. Приведенный ниже текучий API задает правило
ограничения и отключает правило каскадного удаления.
C#
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Свойства внешнего ключа и навигации сущности
Enrollment
Запись зачисления обозначает один курс, который проходит один учащийся.
Обновите Models/Enrollment.cs , включив в него следующий код.
C#
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство
первичного ключа CourseID и свойство навигации Course :
C#
public int CourseID { get; set; }
public Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно
свойство первичного ключа StudentID и свойство навигации Student :
C#
public int StudentID { get; set; }
public Student Student { get; set; }
Связи многие ко многим
Между сущностями Student и Course действует связь многие ко многим. Сущности
Enrollment выступают в качестве таблицы соединения "многие ко многим" с
полезными данными в базе данных. Фраза с полезными данными означает, что
таблица Enrollment содержит дополнительные данные, кроме внешних ключей для
присоединяемых таблиц. В сущности Enrollment дополнительными данными
помимо внешних ключей являются PK и Grade .
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта
схема была создана с помощью EF Power Tools
не входит в этот учебник.)
для EF 6.x. Процедура ее создания
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая
характер один ко многим.
Если таблица Enrollment не включала в себя сведения об оценках, потребуется,
чтобы она содержала всего два внешних ключа: CourseID и StudentID . Таблицу
соединения многие ко многим без полезных данных иногда называют чистой
таблицей соединения (PJT).
Сущности Instructor и Course имеют связь "многие ко многим" с использованием
PJT.
Обновление контекста базы данных
Обновите Data/SchoolContext.cs , включив в него следующий код.
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
public
public
public
public
public
public
DbSet<Course> Courses { get; set; }
DbSet<Enrollment> Enrollments { get; set;
DbSet<Student> Students { get; set; }
DbSet<Department> Departments { get; set;
DbSet<Instructor> Instructors { get; set;
DbSet<OfficeAssignment> OfficeAssignments
}
}
}
{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable(nameof(Course))
.HasMany(c => c.Instructors)
.WithMany(i => i.Courses);
modelBuilder.Entity<Student>().ToTable(nameof(Student));
modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
}
}
}
Приведенный выше код добавляет новые сущности и настраивает связь "многие ко
многим" между сущностями Instructor и Course .
Текучий API вместо атрибутов
Метод OnModelCreating в предыдущем коде использует текучий API для настройки
EF Core поведения. Этот API называется "текучим", так как часто используется для
объединения серии вызовов методов в один оператор. В следующем коде показан
пример текучего API:
C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
В этом учебнике текучий API используется только для сопоставления базы данных,
которое невозможно выполнить с помощью атрибутов. Однако текучий API
позволяет задать большинство правил форматирования, проверки и
сопоставления, которые можно указать с помощью атрибутов.
Некоторые атрибуты, такие как MinimumLength , невозможно применить с текучим
API. MinimumLength не изменяет схему, он лишь применяет правило проверки
минимальной длины.
Некоторые разработчики предпочитают использовать текучий API монопольно,
чтобы оставить свои классы сущностей чистыми. Атрибуты и текучий API можно
смешивать. Существует несколько конфигураций, которые можно реализовать
только с помощью текучего API, например указание составного первичного ключа.
Существует несколько конфигураций, которые можно реализовать только с
помощью атрибутов ( MinimumLength ). Рекомендации по использованию текучего
API и атрибутов:
Выберите один из двух этих подходов.
Используйте выбранный подход максимально согласованно.
Некоторые атрибуты из этого руководства используются:
только для проверки (например, MinimumLength );
EF Core только конфигурация (например, HasKey ).
Проверка и EF Core настройка (например, [StringLength(50)] ).
Дополнительные сведения о сравнении атрибутов и текучего API см. в разделе
Методы конфигурации.
Заполнение базы данных
Обновите код в Data/DbInitializer.cs :
C#
using
using
using
using
ContosoUniversity.Models;
System;
System.Collections.Generic;
System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// Look for any students.
if (context.Students.Any())
{
return;
// DB has been seeded
}
var alexander = new Student
{
FirstMidName = "Carson",
LastName = "Alexander",
EnrollmentDate = DateTime.Parse("2016-09-01")
};
var alonso = new Student
{
FirstMidName = "Meredith",
LastName = "Alonso",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var anand = new Student
{
FirstMidName = "Arturo",
LastName = "Anand",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var barzdukas = new Student
{
FirstMidName = "Gytis",
LastName = "Barzdukas",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var li = new Student
{
FirstMidName = "Yan",
LastName = "Li",
EnrollmentDate = DateTime.Parse("2018-09-01")
};
var justice = new Student
{
FirstMidName = "Peggy",
LastName = "Justice",
EnrollmentDate = DateTime.Parse("2017-09-01")
};
var norman = new Student
{
FirstMidName = "Laura",
LastName = "Norman",
EnrollmentDate = DateTime.Parse("2019-09-01")
};
var olivetto = new Student
{
FirstMidName = "Nino",
LastName = "Olivetto",
EnrollmentDate = DateTime.Parse("2011-09-01")
};
var students = new Student[]
{
alexander,
alonso,
anand,
barzdukas,
li,
justice,
norman,
olivetto
};
context.AddRange(students);
var abercrombie = new Instructor
{
FirstMidName = "Kim",
LastName = "Abercrombie",
HireDate = DateTime.Parse("1995-03-11")
};
var fakhouri = new Instructor
{
FirstMidName = "Fadi",
LastName = "Fakhouri",
HireDate = DateTime.Parse("2002-07-06")
};
var harui = new Instructor
{
FirstMidName = "Roger",
LastName = "Harui",
HireDate = DateTime.Parse("1998-07-01")
};
var kapoor = new Instructor
{
FirstMidName = "Candace",
LastName = "Kapoor",
HireDate = DateTime.Parse("2001-01-15")
};
var zheng = new Instructor
{
FirstMidName = "Roger",
LastName = "Zheng",
HireDate = DateTime.Parse("2004-02-12")
};
var instructors = new Instructor[]
{
abercrombie,
fakhouri,
harui,
kapoor,
zheng
};
context.AddRange(instructors);
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
Instructor = fakhouri,
Location = "Smith 17" },
new OfficeAssignment {
Instructor = harui,
Location = "Gowan 27" },
new OfficeAssignment {
Instructor = kapoor,
Location = "Thompson 304" }
};
context.AddRange(officeAssignments);
var english = new Department
{
Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = abercrombie
};
var mathematics = new Department
{
Name = "Mathematics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = fakhouri
};
var engineering = new Department
{
Name = "Engineering",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = harui
};
var economics = new Department
{
Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
Administrator = kapoor
};
var departments = new Department[]
{
english,
mathematics,
engineering,
economics
};
context.AddRange(departments);
var chemistry = new Course
{
CourseID = 1050,
Title = "Chemistry",
Credits = 3,
Department = engineering,
Instructors = new List<Instructor> { kapoor, harui }
};
var microeconomics = new Course
{
CourseID = 4022,
Title = "Microeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var macroeconmics = new Course
{
CourseID = 4041,
Title = "Macroeconomics",
Credits = 3,
Department = economics,
Instructors = new List<Instructor> { zheng }
};
var calculus = new Course
{
CourseID = 1045,
Title = "Calculus",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { fakhouri }
};
var trigonometry = new Course
{
CourseID = 3141,
Title = "Trigonometry",
Credits = 4,
Department = mathematics,
Instructors = new List<Instructor> { harui }
};
var composition = new Course
{
CourseID = 2021,
Title = "Composition",
Credits = 3,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var literature = new Course
{
CourseID = 2042,
Title = "Literature",
Credits = 4,
Department = english,
Instructors = new List<Instructor> { abercrombie }
};
var courses = new Course[]
{
chemistry,
microeconomics,
macroeconmics,
calculus,
trigonometry,
composition,
literature
};
context.AddRange(courses);
var enrollments = new Enrollment[]
{
new Enrollment {
Student = alexander,
Course = chemistry,
Grade = Grade.A
},
new Enrollment {
Student = alexander,
Course = microeconomics,
Grade = Grade.C
},
new Enrollment {
Student = alexander,
Course = macroeconmics,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = calculus,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = trigonometry,
Grade = Grade.B
},
new Enrollment {
Student = alonso,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = anand,
Course = chemistry
},
new Enrollment {
Student = anand,
Course = microeconomics,
Grade = Grade.B
},
new Enrollment {
Student = barzdukas,
Course = chemistry,
Grade = Grade.B
},
new Enrollment {
Student = li,
Course = composition,
Grade = Grade.B
},
new Enrollment {
Student = justice,
Course = literature,
Grade = Grade.B
}
};
context.AddRange(enrollments);
context.SaveChanges();
}
}
}
Предыдущий код предоставляет начальные данные для новых сущностей.
Основная часть кода создает объекты сущностей и загружает демонстрационные
данные. Демонстрационные данные используются для проверки.
Применение миграции либо удаление и
повторное создание
Существующую базу данных можно изменить, использую один из следующих двух
подходов.
Удаление и повторное создание базы данных. Выберите этот раздел, при
использовании SQLite.
Применение миграции к существующей базе данных. Инструкции в этом
разделе подходят только для SQL Server, но не для SQLite.
Для SQL Server применимы оба подхода. Хотя метод с применением миграции
является более сложным и трудоемким, в реальной рабочей среде лучше
использовать именно его.
Удаление и повторное создание базы
данных
Чтобы принудительно EF Core создать новую базу данных, удалите и обновите ее:
Visual Studio
Удалите папку Migrations.
Выполните следующие команды в консоли диспетчера пакетов (PMC):
PowerShell
Drop-Database
Add-Migration InitialCreate
Update-Database
Запустите приложение. При запуске приложения выполняется метод
DbInitializer.Initialize . DbInitializer.Initialize заполняет новую базу данных.
Visual Studio
Откройте базу данных в SSOX.
Если SSOX был открыт ранее, нажмите кнопку Обновить.
Разверните узел Таблицы. Отображаются созданные таблицы.
Следующие шаги
В следующих двух учебниках рассказывается о том, как считывать и обновлять
связанные данные.
Предыдущий учебник
Следующий учебник
Часть 6. Razor Страницы с EF Core
ASP.NET Core — чтение связанных
данных
Статья • 28.01.2023 • Чтение занимает 36 мин
Авторы: Том Дайкстра
Андерсон
(Tom Dykstra), Йон П. Смит
(Jon P Smith) и Рик
(Rick Anderson)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
Этот учебник посвящен чтению и отображению связанных данных. Связанные
данные — это данные, EF Core которые загружаются в свойства навигации.
На следующих рисунках изображены готовые страницы для этого руководства:
Безотложная, явная и отложенная загрузка
Существует несколько способов загрузки EF Core связанных данных в свойства
навигации сущности:
Безотложная загрузка. Безотложной является загрузка, когда запрос для
одного типа сущности также загружает связанные сущности. При чтении
сущности извлекаются ее связанные данные. Обычно такая загрузка
представляет собой одиночный запрос с соединением, который получает все
необходимые данные. EF Core будет выдавать несколько запросов для
некоторых типов безотложной загрузки. Отправка нескольких запросов
может оказаться эффективнее, чем выполнение одного большого запроса.
Безотложная загрузка указывается с помощью методов Include и ThenInclude.
Безотложная загрузка отправляет несколько запросов, когда включена
навигация коллекции:
Один запрос для основного запроса
Один запрос для каждого "края" коллекции в дереве загрузки.
Отдельные запросы с Load помощью: данные можно получить в отдельных
запросах и EF Core "исправляет" свойства навигации. "Исправления" означает,
что EF Core автоматически заполняет свойства навигации. Отдельные запросы
с Load больше похожи на явную загрузку, чем на безотложную.
Примечание:EF Core автоматически исправляет свойства навигации для
любых других сущностей, которые ранее были загружены в экземпляр
контекста. Даже если данные для свойства навигации не включены явно,
свойство все равно можно заполнить при условии, что ранее были загружены
некоторые или все связанные сущности.
Явная загрузка. При первом чтении сущности связанные данные не
извлекаются. Нужно написать код, извлекающий связанные данные, когда они
необходимы. Явная загрузка с отдельными запросами приводит к отправке
нескольких запросов к базе данных. При явной загрузке код указывает, какие
свойства навигации нужно загрузить. Для выполнения явной загрузки
используется метод Load . Пример:
Отложенная загрузка. При первом чтении сущности связанные данные не
извлекаются. При первом обращении к свойству навигации необходимые для
этого свойства данные извлекаются автоматически. Запрос к базе данных
отправляется при каждом первом обращении к свойству навигации.
Отложенная загрузка может снизить производительность, например когда
разработчики используют запросы N+1 , загружая родительский объект и
перечисляя дочерние объекты.
Создание страниц курсов
Сущность Course включает в себя свойство навигации, содержащее связанную
сущность Department .
Чтобы отобразить имя кафедры, которой назначен курс, выполните указанные
ниже действия.
Загрузите связанную сущность Department в свойство навигации
Course.Department .
Получите имя из свойства Name сущности Department .
Формирование шаблона для страниц курсов
Visual Studio
Следуйте инструкциям в разделе Формирование шаблона для страниц
Student, за исключением следующего:
Создайте папку Pages/Courses.
Используйте класс модели Course .
Используйте существующий класс контекста вместо создания нового.
Откройте Pages/Courses/Index.cshtml.cs и проверьте метод OnGetAsync .
Подсистема формирования шаблонов указала безотложную загрузку для
свойства навигации Department . Метод Include задает безотложную загрузку.
Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры
отображается бесполезный DepartmentID .
Отображение названия кафедры
Измените файл Pages/Courses/Index.cshtml.cs, используя следующий код:
C#
using
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
System.Collections.Generic;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IList<Course> Courses { get; set; }
public async Task OnGetAsync()
{
Courses = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
}
}
Приведенный выше код изменяет свойство Course на Courses и добавляет
AsNoTracking . AsNoTracking повышает производительность, так как возвращаемые
сущности не отслеживаются. Отслеживать сущности не нужно, так как они не
изменяются в текущем контексте.
Обновите Pages/Courses/Index.cshtml , включив в него следующий код.
CSHTML
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h1>Courses</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
<th>
@Html.DisplayNameFor(model
</th>
=> model.Courses[0].CourseID)
=> model.Courses[0].Title)
=> model.Courses[0].Credits)
=> model.Courses[0].Department)
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Courses)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.CourseID">Edit</a>
|
<a asp-page="./Details" asp-routeid="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
В шаблонный код были внесены следующие изменения:
Имя свойства Course изменилось на Courses .
Добавлен столбец Number (Номер), отображающий значение свойства
CourseID . По умолчанию в шаблоне отсутствуют первичные ключи, поскольку
для конечных пользователей они не имеют смысла. Однако в нашем случае
первичный ключ имеет смысл.
Изменен столбец Department (Кафедра) для отображения названия кафедры.
Код отображает свойство Name сущности Department , которая загружена в
свойство навигации Department :
HTML
@Html.DisplayFor(modelItem => item.Department.Name)
Для просмотра списка с названиями кафедр запустите приложение и выберите
вкладку Courses (Курсы).
Загрузка связанных данных с помощью "Select"
Метод OnGetAsync загружает связанные данные с помощью метода Include . Метод
Select является альтернативным вариантом, который загружает только
необходимые связанные данные. Для отдельных элементов, таких как
Department.Name , используется SQL INNER JOIN . Для коллекций используется доступ к
другой базе данных, но это же делает и оператор Include в коллекциях.
Следующий код загружает связанные данные с помощью метода Select :
C#
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
Приведенный выше код не возвращает никаких типов сущностей, поэтому
отслеживание не выполняется. Дополнительные сведения об отслеживании EF см.
в разделе Отслеживание или Отключение отслеживания запросов.
CourseViewModel :
C#
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Полное решение Razor Pages см. здесь .
Создание страниц преподавателей
В этом разделе формируется шаблон для страниц преподавателей, а затем на
страницу указателя преподавателей добавляются связанные курсы и регистрации.
Эта страница считывает и отображает связанные данные следующим образом:
Список преподавателей отображает связанные данные из сущности
OfficeAssignment ("Office" (Кабинет) на предыдущем изображении). Между
сущностями Instructor и OfficeAssignment действует связь один к нулю или к
одному. Безотложная загрузка используется для сущностей OfficeAssignment .
Безотложная загрузка обычно более эффективна, когда требуется отобразить
связанные данные. В этом случае отображается принадлежность к кабинету
для каждого преподавателя.
Когда пользователь выбирает преподавателя, отображаются связанные
сущности Course . Между сущностями Instructor и Course действует связь
многие ко многим. Используется безотложная загрузка для сущностей Course
и связанных сущностей Department . В этом случае отдельные запросы могут
оказаться эффективнее, так как требуются только курсы для выбранного
преподавателя. Этот пример показывает, как использовать безотложную
загрузку для свойств навигации в сущностях, находящихся в свойствах
навигации.
Когда пользователь выбирает курс, отображаются связанные данные из
сущности Enrollments . На предыдущем изображении отображается имя и
оценка учащегося. Между сущностями Course и Enrollment действует связь
один ко многим.
Создание модели представления
На странице "Instructors" (Преподаватели) отображаются данные из трех различных
таблиц. Требуется модель представления, которая включает в себя три свойства,
представляющие три таблицы.
Создайте Models/SchoolViewModels/InstructorIndexData.cs , используя следующий
код:
C#
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Формирование шаблона для страниц преподавателей
Visual Studio
Следуйте инструкциям в разделе Формирование шаблона для страниц
Student, за исключением следующего:
Создайте папку Pages/Instructors.
Используйте класс модели Instructor .
Используйте существующий класс контекста вместо создания нового.
Запустите приложение и перейдите на страницу Instructors.
Обновите Pages/Instructors/Index.cshtml.cs , включив в него следующий код.
C#
using
using
using
using
using
using
using
ContosoUniversity.Models;
ContosoUniversity.Models.SchoolViewModels;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
// Add VM
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await
_context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
}
}
}
Метод OnGetAsync принимает необязательные данные маршрутизации для
идентификатора выбранного преподавателя.
Изучите запрос в файле Pages/Instructors/Index.cshtml.cs :
C#
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
В коде задается безотложная загрузка для следующих свойств навигации:
Instructor.OfficeAssignment
Instructor.Courses
Course.Department
Приведенный ниже код выполняется при выборе преподавателя ( id != null ).
C#
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
Выбранный преподаватель извлекается из списка преподавателей в модели
представления. Свойство модели представления Courses вместе с сущностями
Course загружается из свойства навигации Courses выбранного преподавателя.
Метод Where возвращает коллекцию. В этом случае фильтр выберет одну сущность,
поэтому вызывается метод Single для преобразования коллекции в одну сущность
Instructor . Сущность Instructor предоставляет доступ к свойству навигации
Course .
Метод Single используется для коллекции, когда она содержит всего один элемент.
Метод Single вызывает исключение, если коллекция пуста или содержит больше
одного элемента. Альтернативой является SingleOrDefault с возвратом который
значения по умолчанию, если коллекция пустая. Для этого запроса null
возвращается по умолчанию.
Следующий код заполняет свойство Enrollments модели представления при
выборе курса:
C#
if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await _context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
Изменение страницы индекса преподавателей
Обновите Pages/Instructors/Index.cshtml , включив в него следующий код.
CSHTML
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.InstructorData.Instructors)
{
string selectedRow = "";
if (item.ID == Model.InstructorID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
<td>
<a asp-page="./Index" asp-route-id="@item.ID">Select</a>
|
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.ID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.InstructorData.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.InstructorData.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-routecourseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
@if (Model.InstructorData.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.InstructorData.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
Приведенный выше код вносит следующие изменения:
Изменяет директиву page на @page "{id:int?}" . "{id:int?}" является
шаблоном маршрута. Шаблон маршрута изменяет целочисленные строки
запроса в URL-адресе для маршрутизации данных. Например, при выборе
ссылки Select для преподавателя только с директивой @page формируется
URL-адрес следующего вида:
https://localhost:5001/Instructors?id=2
Когда используется директива страницы @page "{id:int?}" , URL-адрес имеет
следующее значение: https://localhost:5001/Instructors/2 .
Добавляет столбец Office, в котором отображается
item.OfficeAssignment.Location только тогда, когда item.OfficeAssignment не
равно NULL. Так как это связь один к нулю или одному, то связанная сущность
OfficeAssignment может отсутствовать.
HTML
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
Добавляет столбец Courses, содержащий курсы, которые ведет конкретный
преподаватель. Подробные сведения об использовании синтаксиса Razor см.
в разделе Явный перенос строки.
Добавляет код, который динамически добавляет class="table-success" к
элементу tr выбранного преподавателя и курса. Этот параметр задает цвет
фона для выделенных строк c помощью класса Bootstrap.
HTML
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
Добавляет новую гиперссылку с меткой Select (Выбрать). Она отправляет
идентификатор выбранного преподавателя в метод Index и задает цвет фона.
HTML
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Добавляет таблицу курсов для выбранного преподавателя.
Добавляет таблицу регистраций учащихся для выбранного курса.
Запустите приложение и выберите вкладку Instructors. На странице отображается
Location (кабинет) из связанной сущности OfficeAssignment . Если OfficeAssignment
имеет значение NULL, отображается пустая ячейка таблицы.
Щелкните ссылку Select для преподавателя. Стиль строки изменится, и отобразятся
курсы, назначенные этому преподавателю.
Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.
Дальнейшие действия
Следующее руководство посвящено обновлению связанных данных.
Предыдущий учебник
Следующий учебник
Часть 7. Razor Страницы с EF Core
ASP.NET Core — обновление
связанных данных
Статья • 28.01.2023 • Чтение занимает 46 мин
Авторы: Том Дайкстра
Андерсон
(Tom Dykstra), Йон П. Смит
(Jon P Smith) и Рик
(Rick Anderson)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
Этот учебник посвящен обновлению связанных данных. На рисунках ниже
показаны некоторые готовые страницы.
Обновление страниц создания и
редактирования курсов
Шаблонный код для страниц Course Create и Course Edit содержит
раскрывающийся список Department с DepartmentID , int . В этом раскрывающемся
списке должны отображаться названия кафедр, поэтому для каждой из этих
страниц требуется список названий кафедр. Чтобы предоставить этот список,
используйте базовый класс для страниц создания и редактирования.
Создание базового класса для создания и
редактирования курсов
Создайте файл Pages/Courses/DepartmentNamePageModel.cs со следующим кодом:
C#
using
using
using
using
using
using
ContosoUniversity.Data;
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.AspNetCore.Mvc.Rendering;
Microsoft.EntityFrameworkCore;
System.Linq;
namespace ContosoUniversity.Pages.Courses
{
public class DepartmentNamePageModel : PageModel
{
public SelectList DepartmentNameSL { get; set; }
public void PopulateDepartmentsDropDownList(SchoolContext _context,
object selectedDepartment = null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name // Sort by name.
select d;
DepartmentNameSL = new
SelectList(departmentsQuery.AsNoTracking(),
nameof(Department.DepartmentID),
nameof(Department.Name),
selectedDepartment);
}
}
}
Приведенный выше код создает SelectList, содержащий список названий кафедр.
Если указан параметр selectedDepartment , кафедра выбрана в списке SelectList .
Классы моделей страниц Create и Edit являются производными от
DepartmentNamePageModel .
Обновление модели для страницы создания курсов
Курс назначается кафедре. Базовый класс для страниц создания и редактирования
предоставляет список SelectList для выбора кафедры. Раскрывающийся список,
использующий SelectList , устанавливает свойство внешнего ключа
Course.DepartmentID . EF Core Course.DepartmentID использует FK для загрузки
свойства навигации Department .
Обновите Pages/Courses/Create.cshtml.cs , включив в него следующий код.
C#
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class CreateModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public CreateModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IActionResult OnGet()
{
PopulateDepartmentsDropDownList(_context);
return Page();
}
[BindProperty]
public Course Course { get; set; }
public async Task<IActionResult> OnPostAsync()
{
var emptyCourse = new Course();
if (await TryUpdateModelAsync<Course>(
emptyCourse,
"course",
// Prefix for form value.
s => s.CourseID, s => s.DepartmentID, s => s.Title, s =>
s.Credits))
{
_context.Courses.Add(emptyCourse);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// Select DepartmentID if TryUpdateModelAsync fails.
PopulateDepartmentsDropDownList(_context,
emptyCourse.DepartmentID);
return Page();
}
}
}
Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от
английского, сообщите нам на странице обсуждения этой проблемы на сайте
GitHub .
Предыдущий код:
Происходит от DepartmentNamePageModel .
Использует TryUpdateModelAsync, чтобы предотвратить чрезмерную передачу
данных.
Удаляет ViewData["DepartmentID"] . DepartmentNameSL SelectList — это строго
типизированная модель, которая будет использоваться страницей Razor.
Вместо слабо типизированных моделей рекомендуется использовать строго
типизированные. Дополнительные сведения см. в разделе Слабо
типизированные данные (ViewData и ViewBag).
Обновление страницы Razor создания курсов
Обновите Pages/Courses/Create.cshtml , включив в него следующий код.
CSHTML
@page
@model ContosoUniversity.Pages.Courses.CreateModel
@{
ViewData["Title"] = "Create Course";
}
<h2>Create</h2>
<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label">
</label>
<input asp-for="Course.CourseID" class="form-control" />
<span asp-validation-for="Course.CourseID" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label">
</label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label">
</label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="textdanger" />
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary"
/>
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Приведенный выше код вносит следующие изменения:
Изменяет заголовок с DepartmentID на Department.
Заменяет "ViewBag.DepartmentID" на DepartmentNameSL (из базового класса).
Добавляет параметр "Select Department" (Выбор кафедры). В результате этого
изменения вместо первой кафедры в раскрывающемся списке отображается
пункт "Select Department" (Выберите кафедру), если кафедра еще не выбрана.
Добавляет сообщение о проверке в том случае, если не выбрана кафедра.
На странице Razor Pages используется вспомогательная функция тега Select.
CSHTML
<div class="form-group">
<label asp-for="Course.Department" class="control-label"></label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="Course.DepartmentID" class="text-danger" />
</div>
Протестируйте страницу создания. На странице Create отображается название, а не
идентификатор кафедры.
Обновление модели для страницы редактирования
курсов
Обновите Pages/Courses/Edit.cshtml.cs , включив в него следующий код.
C#
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.EntityFrameworkCore;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class EditModel : DepartmentNamePageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Course Course { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Course = await _context.Courses
.Include(c => c.Department).FirstOrDefaultAsync(m =>
m.CourseID == id);
if (Course == null)
{
return NotFound();
}
// Select current DepartmentID.
PopulateDepartmentsDropDownList(_context, Course.DepartmentID);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
var courseToUpdate = await _context.Courses.FindAsync(id);
if (courseToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Course>(
courseToUpdate,
"course",
// Prefix for form value.
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// Select DepartmentID if TryUpdateModelAsync fails.
PopulateDepartmentsDropDownList(_context,
courseToUpdate.DepartmentID);
return Page();
}
}
}
Изменения аналогичны внесенным в модель страницы Create. В приведенном
выше коде PopulateDepartmentsDropDownList передает идентификатор кафедры, по
которому выбирается кафедра в раскрывающемся списке.
Обновление страницы Razor редактирования курсов
Обновите Pages/Courses/Edit.cshtml , включив в него следующий код.
CSHTML
@page
@model ContosoUniversity.Pages.Courses.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Course</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Course.CourseID" />
<div class="form-group">
<label asp-for="Course.CourseID" class="control-label">
</label>
<div>@Html.DisplayFor(model => model.Course.CourseID)</div>
</div>
<div class="form-group">
<label asp-for="Course.Title" class="control-label"></label>
<input asp-for="Course.Title" class="form-control" />
<span asp-validation-for="Course.Title" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="Course.Credits" class="control-label">
</label>
<input asp-for="Course.Credits" class="form-control" />
<span asp-validation-for="Course.Credits" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Course.Department" class="control-label">
</label>
<select asp-for="Course.DepartmentID" class="form-control"
asp-items="@Model.DepartmentNameSL"></select>
<span asp-validation-for="Course.DepartmentID" class="textdanger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Приведенный выше код вносит следующие изменения:
Отображает идентификатор курса. Как правило, первичный ключ сущности не
отображается. Первичные ключи для пользователей обычно не имеют
значения. В этом случае в качестве первичного ключа используется номер
курса.
Изменяет заголовок для раскрывающегося списка кафедр с DepartmentID на
Department.
Заменяет "ViewBag.DepartmentID" на DepartmentNameSL , которая находится в
базовом классе.
На этой странице содержится скрытое поле ( <input type="hidden"> ) с номером
курса. Добавление вспомогательной функции тега <label> с aspfor="Course.CourseID" не избавляет от необходимости использовать это скрытое
поле. <input type="hidden"> необходимо, чтобы включить номер курса в
отправляемые данные, когда пользователь нажимает кнопку Сохранить.
Обновление моделей для страниц курсов
Применение AsNoTracking позволяет повысить производительность в тех
сценариях, где не требуется отслеживание.
Обновите Pages/Courses/Delete.cshtml.cs и Pages/Courses/Details.cshtml.cs ,
добавив AsNoTracking к методам OnGetAsync :
C#
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Course = await _context.Courses
.AsNoTracking()
.Include(c => c.Department)
.FirstOrDefaultAsync(m => m.CourseID == id);
if (Course == null)
{
return NotFound();
}
return Page();
}
Обновление страницы Razor курсов
Обновите Pages/Courses/Delete.cshtml , включив в него следующий код.
CSHTML
@page
@model ContosoUniversity.Pages.Courses.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Department.Name)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Course.CourseID" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Выполните те же изменения для страницы Details.
CSHTML
@page
@model ContosoUniversity.Pages.Courses.DetailsModel
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Course.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Course.Department.Name)
</dd>
</dl>
</div>
<div>
<a asp-page="./Edit" asp-route-id="@Model.Course.CourseID">Edit</a> |
<a asp-page="./Index">Back to List</a>
</div>
Тестирование страниц курса
Протестируйте страницы создания, редактирования, сведений и удаления.
Обновление страниц создания и
редактирования преподавателей
Преподаватели могут вести любое число курсов. На рисунке ниже показана
страница редактирования преподавателей с массивом флажков курсов.
С помощью флажков можно изменять курсы, которым назначен преподаватель.
Флажок отображается для каждого курса в базе данных. Для курсов, которым
назначен преподаватель, флажок установлен. Пользователь может устанавливать и
снимать флажки, изменяя назначения курсов. Если число курсов было бы гораздо
больше, лучше подошел бы другой пользовательский интерфейс. Однако
представленный здесь способ управления связями "многие ко многим" был бы тем
же. Для создания или удаления связей производятся операции с сущностью
соединения.
Создание класса для данных по назначенным курсам
Создайте Models/SchoolViewModels/AssignedCourseData.cs , используя следующий код:
C#
namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
Класс AssignedCourseData содержит данные для создания флажков, определяющих
назначенные преподавателю курсы.
Создание базового класса модели для страницы
преподавателя
Создайте базовый класс Pages/Instructors/InstructorCoursesPageModel.cs :
C#
using
using
using
using
using
using
ContosoUniversity.Data;
ContosoUniversity.Models;
ContosoUniversity.Models.SchoolViewModels;
Microsoft.AspNetCore.Mvc.RazorPages;
System.Collections.Generic;
System.Linq;
namespace ContosoUniversity.Pages.Instructors
{
public class InstructorCoursesPageModel : PageModel
{
public List<AssignedCourseData> AssignedCourseDataList;
public void PopulateAssignedCourseData(SchoolContext context,
Instructor instructor)
{
var allCourses = context.Courses;
var instructorCourses = new HashSet<int>(
instructor.Courses.Select(c => c.CourseID));
AssignedCourseDataList = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
AssignedCourseDataList.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
}
}
}
InstructorCoursesPageModel — это базовый класс, используемый для моделей
страниц Edit и Create. PopulateAssignedCourseData считывает все сущности Course
для заполнения списка AssignedCourseDataList . Для каждого курса код задает
CourseID , название, а также сведения о назначении курсу преподавателя. Для
эффективного поиска используется класс HashSet.
Обработка расположения кабинета
Еще одна связь, с которой должна работать страница редактирования, — это связь
"один к нулю или к одному" между сущностью Instructor и сущностью
OfficeAssignment . Код изменения преподавателя должен обеспечивать указанные
ниже сценарии.
Если пользователь удаляет назначение кабинета, удаляется сущность
OfficeAssignment .
Если пользователь вводит назначение пустого кабинета, создается новая
сущность OfficeAssignment .
Если пользователь изменяет назначение кабинета, обновляется сущность
OfficeAssignment .
Обновление модели для страницы
редактирования преподавателя
Обновите Pages/Instructors/Edit.cshtml.cs , включив в него следующий код.
C#
using
using
using
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.EntityFrameworkCore;
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class EditModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (Instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(_context, Instructor);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id, string[]
selectedCourses)
{
if (id == null)
{
return NotFound();
}
var instructorToUpdate = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.FirstOrDefaultAsync(s => s.ID == id);
if (instructorToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
if (String.IsNullOrWhiteSpace(
instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses,
instructorToUpdate);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(_context, instructorToUpdate);
return Page();
}
public void UpdateInstructorCourses(string[] selectedCourses,
Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
var courseToRemove =
instructorToUpdate.Courses.Single(
c => c.CourseID ==
course.CourseID);
instructorToUpdate.Courses.Remove(courseToRemove);
}
}
}
}
}
}
Предыдущий код:
Получает текущую сущность Instructor из базы данных, используя
безотложную загрузку для свойств навигации OfficeAssignment и Courses .
Обновляет извлеченную сущность Instructor , используя значения из
связывателя модели. TryUpdateModelAsync позволяет предотвратить
чрезмерную передачу данных.
Если расположение кабинета пусто, Instructor.OfficeAssignment получает
значение NULL. Если Instructor.OfficeAssignment имеет значение NULL,
связанная строка в таблице OfficeAssignment удаляется.
Вызывает PopulateAssignedCourseData в OnGetAsync , чтобы предоставить
сведения для флажков с помощью класса модели представления
AssignedCourseData .
Вызывает UpdateInstructorCourses в OnPostAsync , чтобы применить сведения
флажков к редактируемой сущности Instructor.
Вызывает PopulateAssignedCourseData и UpdateInstructorCourses в OnPostAsync
в случае сбоя TryUpdateModelAsync. Эти вызовы методов восстанавливают
данные по назначенным курсам, введенные на странице, при ее повторном
отображении с сообщением об ошибке.
Так как страница Razor не содержит коллекцию сущностей Course, связыватель
модели не может автоматически обновить свойство навигации Courses . Вместо
использования связывателя модели обновление свойства навигации Courses
можно выполнить в новом методе UpdateInstructorCourses . Поэтому нужно
исключить свойство Courses из привязки модели. Это не требует внесения никаких
изменений в код, вызывающий TryUpdateModelAsync, так как вы используете
перегрузку с объявленными свойствами, а Courses отсутствует в списке включений.
Если не установить флажки, код в UpdateInstructorCourses инициализирует
instructorToUpdate.Courses с использованием пустой коллекции и возвращает
следующее:
C#
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
После этого код в цикле проходит по всем курсам в базе данных и сравнивает
каждый из них с теми, которые сейчас назначены преподавателю, в
противоположность тем, которые были выбраны на странице. Чтобы упростить
эффективную подстановку, последние две коллекции хранятся в объектах HashSet .
Если флажок для курса установлен, но курс отсутствует в свойстве навигации
Instructor.Courses , этот курс добавляется в коллекцию в свойстве навигации.
C#
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
Если флажок для курса не установлен, но курс присутствует в свойстве навигации
Instructor.Courses , этот курс удаляется из свойства навигации.
C#
else
{
if (instructorCourses.Contains(course.CourseID))
{
var courseToRemove = instructorToUpdate.Courses.Single(
c => c.CourseID == course.CourseID);
instructorToUpdate.Courses.Remove(courseToRemove);
}
}
Обновление страницы Razor редактирования
преподавателя
Обновите Pages/Instructors/Edit.cshtml , включив в него следующий код.
CSHTML
@page
@model ContosoUniversity.Pages.Instructors.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Instructor.ID" />
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label">
</label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="controllabel"></label>
<input asp-for="Instructor.FirstMidName" class="formcontrol" />
<span asp-validation-for="Instructor.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label">
</label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location"
class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location"
class="form-control" />
<span asp-validationfor="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="table">
<table>
<tr>
@{
int cnt = 0;
foreach (var course in
Model.AssignedCourseDataList)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @:
@course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Приведенный выше код создает таблицу HTML с тремя столбцами. Каждый столбец
содержит флажок и заголовок с номером и названием курса. Все флажки имеют
одинаковые имена ("selectedCourses"). Поскольку используется одно и то же имя,
связыватель модели интерпретирует их как группу. Атрибуту значения для каждого
флажка присвоен CourseID . При отправке страницы связыватель модели передает
массив, содержащий значения CourseID только для установленных флажков.
При первичной отрисовке флажков выбираются курсы, назначенные
преподавателю.
Примечание. Описываемый здесь подход к редактированию данных курсов для
преподавателя эффективен при ограниченном числе курсов. Для коллекций
большего размера более практичным и эффективным было бы применение
другого пользовательского интерфейса и другого метода обновления.
Запустите приложение и протестируйте обновленную страницу редактирования
преподавателя. Измените некоторые назначения курсов. Изменения отражаются
на странице указателя.
Обновление страницы создания преподавателя
Обновите модель для страницы Create преподавателя, используя код, аналогичный
коду страницы Edit.
C#
using
using
using
using
using
using
using
using
ContosoUniversity.Data;
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.Logging;
System;
System.Collections.Generic;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class CreateModel : InstructorCoursesPageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
private readonly ILogger<InstructorCoursesPageModel> _logger;
public CreateModel(SchoolContext context,
ILogger<InstructorCoursesPageModel> logger)
{
_context = context;
_logger = logger;
}
public IActionResult OnGet()
{
var instructor = new Instructor();
instructor.Courses = new List<Course>();
// Provides an empty collection for the foreach loop
// foreach (var course in Model.AssignedCourseDataList)
// in the Create Razor page.
PopulateAssignedCourseData(_context, instructor);
return Page();
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnPostAsync(string[]
selectedCourses)
{
var newInstructor = new Instructor();
if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}
// Add selected Courses courses to the new instructor.
foreach (var course in selectedCourses)
{
var foundCourse = await
_context.Courses.FindAsync(int.Parse(course));
if (foundCourse != null)
{
newInstructor.Courses.Add(foundCourse);
}
else
{
_logger.LogWarning("Course {course} not found", course);
}
}
try
{
if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
}
}
Предыдущий код:
Добавляет ведение журнала для предупреждений и сообщений об ошибках.
Вызывает метод Load, который позволяет получить все элементы Course в
одном вызове базы данных. При работе с небольшими коллекциями
использование этого параметра иFindAsync позволяет также улучшить
производительность. FindAsync возвращает отслеживаемую сущность без
необходимости выполнять запрос к базе данных.
C#
public async Task<IActionResult> OnPostAsync(string[] selectedCourses)
{
var newInstructor = new Instructor();
if (selectedCourses.Length > 0)
{
newInstructor.Courses = new List<Course>();
// Load collection with one DB call.
_context.Courses.Load();
}
// Add selected Courses courses to the new instructor.
foreach (var course in selectedCourses)
{
var foundCourse = await
_context.Courses.FindAsync(int.Parse(course));
if (foundCourse != null)
{
newInstructor.Courses.Add(foundCourse);
}
else
{
_logger.LogWarning("Course {course} not found", course);
}
}
try
{
if (await TryUpdateModelAsync<Instructor>(
newInstructor,
"Instructor",
i => i.FirstMidName, i => i.LastName,
i => i.HireDate, i => i.OfficeAssignment))
{
_context.Instructors.Add(newInstructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return RedirectToPage("./Index");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
PopulateAssignedCourseData(_context, newInstructor);
return Page();
}
_context.Instructors.Add(newInstructor) создает Instructor , используя связи
многие ко многим без явного сопоставления таблицы соединения. Функция
использования связей "многие ко многим" добавлена в EF версии 5.0.
Протестируйте страницу создания преподавателя.
Обновите страницу Create преподавателя Razor, используя код, аналогичный коду
страницы Edit.
CSHTML
@page
@model ContosoUniversity.Pages.Instructors.CreateModel
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Instructor</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Instructor.LastName" class="control-label">
</label>
<input asp-for="Instructor.LastName" class="form-control" />
<span asp-validation-for="Instructor.LastName" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.FirstMidName" class="controllabel"></label>
<input asp-for="Instructor.FirstMidName" class="formcontrol" />
<span asp-validation-for="Instructor.FirstMidName"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.HireDate" class="control-label">
</label>
<input asp-for="Instructor.HireDate" class="form-control" />
<span asp-validation-for="Instructor.HireDate" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Instructor.OfficeAssignment.Location"
class="control-label"></label>
<input asp-for="Instructor.OfficeAssignment.Location"
class="form-control" />
<span asp-validationfor="Instructor.OfficeAssignment.Location" class="text-danger" />
</div>
<div class="form-group">
<div class="table">
<table>
<tr>
@{
int cnt = 0;
foreach (var course in
Model.AssignedCourseDataList)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @:
@course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary"
/>
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Обновление страницы удаления
преподавателя
Обновите Pages/Instructors/Delete.cshtml.cs , включив в него следующий код.
C#
using
using
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Instructor Instructor { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor = await _context.Instructors.FirstOrDefaultAsync(m =>
m.ID == id);
if (Instructor == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Instructor instructor = await _context.Instructors
.Include(i => i.Courses)
.SingleAsync(i => i.ID == id);
if (instructor == null)
{
return RedirectToPage("./Index");
}
var departments = await _context.Departments
.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Приведенный выше код вносит следующие изменения:
Использует упреждающую загрузку для свойства навигации Courses .
Требуется включить Courses , иначе они не будут удалены при удалении
преподавателя. Чтобы избежать необходимости считывать их, настройте
каскадное удаление в базе данных.
Если преподаватель, которого требуется удалить, назначен в качестве
администратора любой из кафедр, удаляется назначение преподавателя из
таких кафедр.
Запустите приложение и протестируйте страницу удаления.
Следующие шаги
Предыдущий учебник
Следующий учебник
Часть 8. Razor Страницы с EF Core
ASP.NET Core — параллелизм
Статья • 28.01.2023 • Чтение занимает 57 мин
Том Дайкстра
(Tom Dykstra), Йон П. Смит
(Jon P Smith)
Веб-приложение Университета Contoso демонстрирует создание Razor вебприложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств
см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое
приложение
и сравните его код с тем, который вы создали в процессе работы с
этим руководством.
Это руководство описывает, как обрабатывать конфликты, когда несколько
пользователей параллельно изменяют одну сущность.
Конфликты параллелизма
Конфликт параллелизма возникает в следующих условиях:
Пользователь переходит на страницу редактирования для сущности.
Другой пользователь обновляет ту же сущность до того, как изменение
первого пользователя записывается в базу данных.
Если обнаружение параллелизма не включено, то пользователь, обновляющий
базу данных последним, перезаписывает изменения другого пользователя. Если
такой риск допустим, стоимость реализации параллелизма может перевесить его
преимущества.
Пессимистичный параллелизм
Один из способов предотвращения конфликтов параллелизма — блокировка базы
данных. Это называется пессимистичным параллелизмом. Прежде чем приложение
считывает строку базы данных, которую планируется обновить, оно запрашивает
блокировку. Когда строка блокируется для обновления, другие пользователи не
могут заблокировать ее, пока первая блокировка не будет снята.
Управление блокировками имеет недостатки. Его может быть сложно реализовать,
и оно может вызывать проблемы с производительностью по мере увеличения
числа пользователей. Entity Framework Core не предоставляет встроенную
поддержку пессимистичного параллелизма.
Оптимистическая блокировка
Оптимистическая блокировка допускает появление конфликтов параллелизма, а
затем обрабатывает их соответствующим образом. Например, Мария посещает
страницу изменения кафедры и изменяет бюджет кафедры английской языка с
350 000,00 USD на 0,00 USD.
Прежде чем Мария нажимает кнопку Save (Сохранить), Дмитрий заходит на ту же
страницу и изменяет значение в поле "Start Date" (Дата начала) с 9/1/2007 на
9/1/2013.
Сначала Мария нажимает кнопку Save (Сохранить) и видит, что ее изменение
вступило в силу, так как в браузере отображается страница индекса с нулевым
размером бюджета.
Дмитрий нажимает кнопку Save (Сохранить) на странице редактирования, где все
еще отображается бюджет 350 000,00 USD. Дальнейший ход событий определяется
порядком обработки конфликтов параллелизма.
Отслеживайте, для какого свойства пользователь изменил и обновил только
соответствующие столбцы в базе данных.
В этом сценарии никакие данные не теряются. Разные свойства были
обновлены двумя пользователями. Когда какой-либо пользователь
просмотрит кафедру английского языка в следующий раз, он увидит
изменения, внесенные как Марией, так и Дмитрием. Этот способ обновления
позволяет снизить количество конфликтов, способных привести к потере
данных. Такой подход имеет ряд недостатков.
Он не позволяет избежать потери данных, если конкурирующие изменения
вносятся для одного свойства.
Он не слишком хорошо подходит для веб-приложений, так как требует
поддерживать значительный объем состояний, чтобы отслеживать все
извлеченные и новые значения. Обслуживание большого объема
состояний может негативно повлиять на производительность приложения.
Он может повысить сложность приложений по сравнению с обнаружением
параллелизма для сущности.
Позвольте изменению Дмитрия перезаписать изменение Марии.
Когда какой-либо пользователь просмотрит кафедру английского языка в
следующий раз, он увидит дату 9/1/2013 и значение 350 000,00 USD. Такой
подход называется победой клиента или сохранением последнего внесенного
изменения. Все значения из клиента имеют приоритет над данными в
хранилище. Сформированный код не выполняет обработку параллелизма, и
автоматически применяется победа клиента.
Запретите обновление изменения Дмитрия в базе данных. Как правило
приложение будет:
отображать сообщение об ошибке;
отображать текущее состояние данных;
разрешать пользователю повторно применять изменения.
Это называется победой хранилища. Значения в хранилище имеют приоритет
над данными, передаваемыми клиентом. В этом руководстве используется
сценарий победы хранилища. Данный метод гарантирует, что никакие
изменения не перезаписываются без оповещения пользователя.
Обнаружение конфликтов в EF Core
Свойства, настроенные как маркеры параллелизма, используются для реализации
управления оптимистической блокировкой. Если операция обновления или
удаления активируется SaveChanges или SaveChangesAsyncзначение токена
параллелизма в базе данных сравнивается с исходным значением, считываемым:EF
Core
Если значения совпадают, операция может завершиться.
Если значения не совпадают, предполагается, EF Core что другой пользователь
выполнил конфликтующую операцию, прерывает текущую транзакцию и
создает исключение DbUpdateConcurrencyException.
Выполнение другим пользователем или процессом операции, конфликтующей с
текущей операцией, называется конфликтом параллелизма.
В реляционных базах данных EF Core проверяет значение маркера параллелизма в
WHERE предложении UPDATE и DELETE инструкциях для обнаружения конфликта
параллелизма.
Модель данных нужно настроить так, чтобы она включала обнаружение
конфликтов. Для этого добавьте столбец отслеживания, который можно
использовать для определения момента изменения строки. EF предоставляет два
подхода для маркеров параллелизма:
Применение к свойству модели [ConcurrencyCheck] или IsConcurrencyToken.
Этот подход не рекомендуется. Дополнительные сведения см. в разделе
"Токены параллелизма в EF Core".
Применение к маркеру параллелизма в модели классов TimestampAttribute
или IsRowVersion. В этом учебнике используется именно этот подход.
Подход SQL Server и сведения о реализации SQLite немного отличаются. Файл со
сведениями о различиях будет представлен далее в учебнике. На вкладке Visual
Studio отображается подход на основе SQL Server. А на вкладке Visual Studio
Code — подход для баз данных, отличных от SQL Server, таких как SQLite.
Visual Studio
Включите в модель столбец отслеживания, который позволяет
определять, когда была изменена строка.
Примените класс TimestampAttribute к свойству concurrency.
Обновите файл Models/Department.cs , используя следующий выделенный код:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] ConcurrencyToken { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Именно класс TimestampAttribute определяет столбец как столбец
отслеживания параллелизма. Другой способ задания свойства
отслеживания — текучий API.
C#
modelBuilder.Entity<Department>()
.Property<byte[]>("ConcurrencyToken")
.IsRowVersion();
Атрибут [Timestamp] в свойстве entity создает в методе ModelBuilder
следующий код:
C#
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Предыдущий код:
Задает типом свойства ConcurrencyToken массив байтов. byte[] является
обязательным типом для SQL Server.
Вызывает IsConcurrencyToken. Метод IsConcurrencyToken настраивает
свойство как маркер параллелизма. При обновлении значение токена
параллелизма в базе данных сравнивается с исходным значением для
проверки того, что оно не изменилось с момента извлечения экземпляра
из базы данных. Если это значение изменилось,
DbUpdateConcurrencyException создает исключение и изменения не
применяются.
Вызывает метод ValueGeneratedOnAddOrUpdate, который настраивает
свойство ConcurrencyToken так, чтобы значение автоматически
создавалось при добавлении или обновлении сущности.
HasColumnType("rowversion") задает тип rowversion для столбца в базе
данных SQL Server.
В следующем коде показана часть T-SQL, созданная EF Core при обновлении
Department имени:
SQL
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
Предыдущий выделенный код показывает предложение WHERE , содержащее
ConcurrencyToken . Если база данных ConcurrencyToken не соответствует
параметру ConcurrencyToken @p2 , строки не обновляются.
Следующий выделенный код показывает код T-SQL, который подтверждает,
что была обновлена всего одна строка.
SQL
SET NOCOUNT ON;
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT возвращает число строк, затронутых при выполнении
последнего оператора. Если строки не обновляются, EF Core вызывает
исключение DbUpdateConcurrencyException .
Добавление миграции
Добавление свойства ConcurrencyToken изменяет модель данных, которая требует
миграции.
Выполните построение проекта.
Visual Studio
Выполните следующие команды в PMC:
PowerShell
Add-Migration RowVersion
Update-Database
Предыдущие команды:
Создает файл миграции Migrations/{time stamp}_RowVersion.cs .
Изменяет файл Migrations/SchoolContextModelSnapshot.cs . Это изменение
добавляет следующий код в метод BuildModel :
C#
b.Property<byte[]>("ConcurrencyToken")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
Формирование шаблона страниц кафедр
Visual Studio
Следуйте инструкциям в разделе Формирование шаблона для страниц Student,
за исключением следующего:
Создайте папку Pages/Departments.
Используйте класс модели Department .
Используйте существующий класс контекста вместо создания нового.
Добавление класса utility
В папке проекта создайте класс Utility , содержащий следующий код:
Visual Studio
C#
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
Класс Utility предоставляет метод GetLastChars , используемый для вывода
последних нескольких символов маркера параллелизма. Следующий код является
кодом, который можно использовать как для SQLite, так и для SQL Server:
C#
#if SQLiteVersion
using System;
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(Guid token)
{
return token.ToString().Substring(
token.ToString().Length - 3);
}
}
}
#else
namespace ContosoUniversity
{
public static class Utility
{
public static string GetLastChars(byte[] token)
{
return token[7].ToString();
}
}
}
#endif
Директива препроцессора #if SQLiteVersion изолирует различия в версиях SQLite и
SQL Server и позволяет:
Авторам использовать для работы с обеими версиями один код.
Разработчикам SQLite развертывать приложение в Azure и использовать
Azure SQL.
Выполните построение проекта.
Обновление страницы индекса
Средство формирования шаблонов создало столбец ConcurrencyToken для
страницы индекса, однако это поле не будет отображаться в рабочем приложении.
В этом учебнике отображается последняя часть ConcurrencyToken для демонстрации
работы параллелизма. Эта последняя часть необязательно должна быть
уникальной.
Обновите страницу Pages\Departments\Index.cshtml:
Замените Index на Departments.
Измените код, содержащий ConcurrencyToken , чтобы отображались только
последние несколько символов.
Замените FirstMidName на FullName .
В следующем примере кода показана обновленная страница:
CSHTML
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model =>
model.Department[0].Administrator)
</th>
<th>
Token
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.Administrator.FullName)
</td>
<td>
@Utility.GetLastChars(item.ConcurrencyToken)
</td>
<td>
<a asp-page="./Edit" asp-routeid="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-routeid="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-routeid="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Обновление модели страницы "Edit"
(Редактирование)
Обновите Pages/Departments/Edit.cshtml.cs , включив в него следующий код.
Visual Studio
C#
using
using
using
using
using
using
using
using
ContosoUniversity.Data;
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.AspNetCore.Mvc.Rendering;
Microsoft.EntityFrameworkCore;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking()
// tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s =>
s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues =
(Department)exceptionEntry.Entity;
var databaseEntry =
exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable
to save. " +
"The department was deleted by another
user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues,
_context);
// Save the current ConcurrencyToken so next
postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken =
(byte[])dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private IActionResult HandleDeletedDepartment()
{
var deletedDepartment = new Department();
// ModelState contains the posted data because of the
deletion error
// and overides the Department instance values when
displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another
user.");
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in
the database "
+ "have been displayed. If you still want to edit this
record, click "
+ "the Save button again.");
}
}
}
Обновления параллелизма
OriginalValue обновляется с помощью значения ConcurrencyToken из сущности при
получении в методе OnGetAsync . EF Core SQL UPDATE создает команду с
предложением WHERE , содержащим исходное ConcurrencyToken значение. Если
команда UPDATE не влияет ни на одну из строк, будет выведено исключение
DbUpdateConcurrencyException . Команда UPDATE может не оказывать влияние на
строки, если нет строк, имеющих исходное значение ConcurrencyToken .
Visual Studio
C#
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;
В предыдущем выделенном коде:
Значение Department.ConcurrencyToken представляет собой значение,
предоставленное во время получения сущности в запросе Get для страницы
Edit . Значение передается в метод OnPost посредством скрытого поля на
странице Razor, на которой отображается редактируемая сущность. Значение
скрытого поля копируется в Department.ConcurrencyToken связывателем
модели.
OriginalValue — это то, что EF Core используется в предложении WHERE .
Перед выполнением выделенной строки кода:
OriginalValue является значением в базе данных на момент вызова
FirstOrDefaultAsync в этом методе.
Это значение может отличаться от значения, отображаемого на странице
Edit.
Выделенный код гарантирует использование EF Core исходного
ConcurrencyToken значения из отображаемой Department сущности в
предложении инструкции WHERE SQL UPDATE .
В следующем примере кода показана модель Department . Department
инициализируется в:
Методе OnGetAsync через запрос EF.
Методе OnPostAsync в скрытом поле на странице Razor с помощью привязки
модели:
Visual Studio
C#
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking()
// tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
// Fetch current department from DB.
// ConcurrencyToken may have changed.
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
return HandleDeletedDepartment();
}
// Set ConcurrencyToken to value read in OnGetAsync
_context.Entry(departmentToUpdate).Property(
d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;
Из приведенного выше кода видно, что значение ConcurrencyToken сущности
Department из запроса HTTP POST равно значению ConcurrencyToken из запроса HTTP
GET .
При возникновении ошибки параллелизма приведенный ниже выделенный код
получает значения клиента (значения, переданные в этот метод) и значения из
базы данных.
C#
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}
Следующий код добавляет пользовательское сообщение об ошибке для каждого
столбца, у которого значения базы данных отличаются от переданных в
OnPostAsync :
C#
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database
"
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
Приведенный ниже выделенный код задает для ConcurrencyToken новое значение,
полученное из базы данных. Когда пользователь в следующий раз нажимает
кнопку Save (Сохранить), перехватываются только те ошибки параллелизма,
возникшие с момента последнего отображения страницы "Edit" (Редактирование).
C#
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current ConcurrencyToken so next postback
// matches unless an new concurrency issue happens.
Department.ConcurrencyToken = dbValues.ConcurrencyToken;
// Clear the model error for the next postback.
ModelState.Remove($"{nameof(Department)}.
{nameof(Department.ConcurrencyToken)}");
}
Оператор ModelState.Remove является обязательным, поскольку ModelState имеет
старое значение ConcurrencyToken . На странице Razor значение ModelState для поля
имеет приоритет над значениями свойств модели, если они присутствуют вместе.
Различия между кодами для SQL Server и SQLite
Ниже вы можете увидеть различия между версиями SQL Server и SQLite:
diff
+ using System;
// For GUID on SQLite
+ departmentToUpdate.ConcurrencyToken = Guid.NewGuid();
_context.Entry(departmentToUpdate)
.Property(d => d.ConcurrencyToken).OriginalValue =
Department.ConcurrencyToken;
- Department.ConcurrencyToken = (byte[])dbValues.ConcurrencyToken;
+ Department.ConcurrencyToken = dbValues.ConcurrencyToken;
Обновление страницы Edit Razor
Обновите Pages/Departments/Edit.cshtml , включив в него следующий код.
CSHTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<div class="form-group">
<label>Version</label>
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label">
</label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label">
</label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="textdanger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label">
</label>
<input asp-for="Department.StartDate" class="form-control"
/>
<span asp-validation-for="Department.StartDate" class="textdanger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="formcontrol"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID"
class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Предыдущий код:
Изменяет директиву page с @page на @page "{id:int}" .
Добавляет версию скрытых строк. Нужно добавить ConcurrencyToken , чтобы
обратная передача привязывала значение.
Отображает последний байт ConcurrencyToken в целях отладки.
Заменяет ViewData на строго типизированный InstructorNameSL .
Тестирование конфликтов параллелизма с
использованием страницы "Edit" (Редактирование)
Откройте в браузере два экземпляра страницы "Edit" (Редактирование) для
кафедры английского языка:
Запустите приложение и выберите "Departments" (Кафедры).
Щелкните правой кнопкой мыши гиперссылку Edit (Изменить) для кафедры
английского языка и выберите пункт Открыть на новой вкладке.
На первой вкладке щелкните гиперссылку Edit (Изменить) для кафедры
английского языка.
На обеих вкладках браузера отображаются одинаковые сведения.
Измените имя на первой вкладке браузера и нажмите кнопку Save (Сохранить).
В браузере отображается страница Index с измененным значением и обновленным
индикатором ConcurrencyToken . Обратите внимание на обновленный индикатор
ConcurrencyToken , он отображается при второй обратной передаче на другой
вкладке.
Измените другое поле на второй вкладке браузера.
Нажмите кнопку Сохранить. Отображаются сообщения об ошибках для всех полей,
которые не соответствуют значениям базы данных:
В этом окне браузера не планировалось изменять поле "Name" (Имя). Скопируйте
и вставьте текущее значение "Languages" (Языки) в поле "Name" (Имя). Выйдите из
поля с помощью клавиши TAB. Проверка на стороне клиента удаляет сообщение
об ошибке.
Снова нажмите кнопку Save (Сохранить). Сохраняется значение, введенное на
второй вкладке браузера. Сохраненные значения отображаются на странице
индекса.
Обновление модели страницы "Delete"
(Удаление)
Обновите Pages/Departments/Delete.cshtml.cs , включив в него следующий код.
C#
using
using
using
using
using
ContosoUniversity.Models;
Microsoft.AspNetCore.Mvc;
Microsoft.AspNetCore.Mvc.RazorPages;
Microsoft.EntityFrameworkCore;
System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool?
concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to
delete "
+ "was modified by another user after you selected delete.
"
+ "The delete operation was canceled and the current
values in the "
+ "database have been displayed. If you still want to
delete this "
+ "record, click the Delete button again.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.ConcurrencyToken value is from when the
entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
Страница "Delete" (Удаление) обнаруживает конфликты параллелизма при
изменении сущности после ее получения. Department.ConcurrencyToken является
версией строки при получении сущности. При EF Core создании SQL DELETE
команды она включает предложение WHERE с ConcurrencyToken . Если команда SQL
DELETE не затрагивает ни одной строки:
ConcurrencyToken в команде SQL DELETE не соответствует ConcurrencyToken в
базе данных.
Возникает исключение DbUpdateConcurrencyException .
Вызывается OnGetAsync с concurrencyError .
Обновление страницы Razor "Delete" (Удаление)
Обновите Pages/Departments/Delete.cshtml , включив в него следующий код.
CSHTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h1>Delete</h1>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.ConcurrencyToken)
</dt>
<dd class="col-sm-10">
@Utility.GetLastChars(Model.Department.ConcurrencyToken)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model =>
model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.ConcurrencyToken" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="./Index">Back to List</a>
</form>
</div>
Приведенный выше код вносит следующие изменения:
Изменяет директиву page с @page на @page "{id:int}" .
Добавляет сообщение об ошибке.
Заменяет FirstMidName на FullName в поле Administrator (Администратор).
Изменяет ConcurrencyToken для отображения последнего байта.
Добавляет версию скрытых строк. Нужно добавить ConcurrencyToken , чтобы
обратная передача привязывала значение.
Тестирование конфликтов параллелизма
Создайте тестовую кафедру.
Откройте в браузере два экземпляра страницы "Delete" (Удаление) для тестовой
кафедры:
Запустите приложение и выберите "Departments" (Кафедры).
Щелкните правой кнопкой мыши гиперссылку Delete (Удалить) для тестовой
кафедры и выберите пункт Открыть на новой вкладке.
Щелкните гиперссылку Edit (Изменить) для тестовой кафедры.
На обеих вкладках браузера отображаются одинаковые сведения.
Измените бюджет на первой вкладке браузера и нажмите кнопку Save (Сохранить).
В браузере отображается страница Index с измененным значением и обновленным
индикатором ConcurrencyToken . Обратите внимание на обновленный индикатор
ConcurrencyToken , он отображается при второй обратной передаче на другой
вкладке.
Удалите тестовую кафедру со второй закладки. Отображается ошибка
параллелизма с текущими значениями из базы данных. При нажатии кнопки Delete
(Удалить) сущность удаляется, если только не был обновлен ConcurrencyToken .
Дополнительные ресурсы
Маркеры параллелизма в EF Core
Обработка параллелизма в EF Core
Отладка источника ASP.NET Core 2.x
Следующие шаги
Это последний учебник из серии. Дополнительные темы рассматриваются в версии
этой серии для MVC.
Предыдущий учебник
ASP.NET Core MVC с EF Core серией
руководств
Статья • 28.01.2023 • Чтение занимает 2 мин
Это руководство описывает MVC-модель ASP.NET Core и Entity Framework Core с
контроллерами и представлениями. Razor Pages — это альтернативная модель
программирования. Для новой разработки мы рекомендуем использовать Razor
Pages, а не MVC с контроллерами и представлениями. См. версию этого
руководства для Razor Pages. В каждом руководстве содержатся уникальные
материалы:
В руководстве по MVC содержатся материалы, которых нет в руководстве по Razor
Pages:
Реализация наследования в модели данных
Выполнение прямых SQL-запросов
Использование динамических запросов LINQ для упрощения кода
В руководстве по Razor Pages содержатся материалы, которых нет в этом
руководстве:
Использование метода Select для загрузки связанных данных
Рекомендации по использованию EF.
1. Начало работы
2. Операции создания, чтения, обновления и удаления
3. Сортировка, фильтрация, разбиение на страницы и группирование данных
4. Миграции
5. Создание сложной модели данных
6. Чтение связанных данных
7. Обновление связанных данных
8. Обработка конфликтов параллелизма
9. Наследование
10. Дополнительные разделы
Руководство. Начало работы с EF Core
в веб-приложении MVC ASP.NET
Статья • 28.01.2023 • Чтение занимает 37 мин
Авторы: Том Дайкстра
(Tom Dykstra) и Рик Андерсон
(Rick Anderson)
Это руководство описывает MVC-модель ASP.NET Core и Entity Framework Core с
контроллерами и представлениями. Razor Pages — это альтернативная модель
программирования. Для новой разработки мы рекомендуем использовать Razor
Pages, а не MVC с контроллерами и представлениями. См. версию этого
руководства для Razor Pages. В каждом руководстве содержатся уникальные
материалы:
В руководстве по MVC содержатся материалы, которых нет в руководстве по Razor
Pages:
Реализация наследования в модели данных
Выполнение прямых SQL-запросов
Использование динамических запросов LINQ для упрощения кода
В руководстве по Razor Pages содержатся материалы, которых нет в этом
руководстве:
Использование метода Select для загрузки связанных данных
Рекомендации по использованию EF.
На примере веб-приложения для университета Contoso демонстрируется процесс
создания веб-приложений ASP.NET Core MVC с помощью Entity Framework (EF) Core
и Visual Studio.
В этом примере приложения реализуется веб-сайт вымышленного университета
Contoso. На нем предусмотрены различные функции, в том числе прием учащихся,
создание курсов и назначение преподавателей. Это первый учебник из серии, в
котором описывается создание примера приложения для университета Contoso.
Предварительные требования
Если у вас нет опыта работы с ASP.NET Core MVC, ознакомьтесь с серией
учебников по началу работы с ASP.NET Core MVC, прежде чем приступать к
изучению этого учебника.
Visual Studio 2022
с рабочей нагрузкой ASP.NET и веб-разработка.
Этот учебник не был обновлен для ASP.NET Core 6 или более поздних версий.
Инструкции руководства будут работать неправильно, если вы создадите проект,
предназначенный для ASP.NET Core 6 или 7. Например, веб-шаблоны ASP.NET Core
6 и 7 используют минимальную модель размещения, которая объединяет
Startup.cs и Program.cs в один Program.cs файл.
Еще одним отличием, представленным в .NET 6, является функция NRT (ссылочные
типы, допускающий значение NULL). Шаблоны проектов поддерживают эту
функцию по умолчанию. Могут возникнуть проблемы, когда EF считает, что
свойство является обязательным в .NET 6, которое допускает значение NULL в .NET
5. Например, страница Create Student (Создание учащегося) завершится сбоем
автоматически, если Enrollments свойство не будет иметь значение NULL или
вспомогательный asp-validation-summary тег не изменится с ModelOnly на All .
В этом руководстве рекомендуется установить и использовать пакет SDK для .NET 5.
Пока это руководство не будет обновлено, смRazor. статью Страницы с Entity
Framework Core в ASP.NET Core . Руководство 1 из 8 о том, как использовать Entity
Framework с ASP.NET Core 6 или более поздней версии.
Ядра СУБД
В инструкциях для Visual Studio используется SQL Server LocalDB, версия
SQL Server Express, которая работает только в Windows.
Решение проблем и устранение неполадок
Если вы столкнулись с проблемами, для их решения можно попробовать сравнить
свой код с кодом готового проекта . Список распространенных ошибок и
способы их устранения см. в разделе "Устранение неполадок" последнего
руководства серии. Если вы не нашли нужные сведения, опубликуйте вопрос
StackOverflow.com для ASP.NET Core
или EF Core .
 Совет
Эта серия включает в себя 10 учебников, содержание каждого из которых
базируется на предыдущих учебниках. После успешного завершения каждого
руководства рекомендуется сохранять копию проекта. Таким образом, при
возникновении проблем вы сможете вернуться к предыдущему учебнику, а не
к началу серии.
Веб-приложение Contoso University
Приложение, создаваемое в этих руководствах, является простым веб-сайтом
университета.
Пользователи приложения могут просматривать и обновлять сведения об
учащихся, курсах и преподавателях. Вот несколько экранов в приложении:
Создание веб-приложения
1. Запустите Visual Studio и щелкните Создать проект
2. В диалоговом окне Создать проект выберите Веб-приложение ASP.NET
Core>Далее.
3. В диалоговом окне Настроить новый проект введите ContosoUniversity в
поле Имя проекта. Очень важно использовать именно такое имя с учетом
регистра символов, чтобы пространства имен ( namespace ) совпадали при
копировании кода.
4. Щелкните Создать.
5. В диалоговом окне Создайте веб-приложение ASP.NET Core сделайте
следующее:
a. В раскрывающихся списках выберите .NET Core и ASP.NET Core 5.0.
b. Щелкните ASP.NET Core Web App (Model-View-Controller) (Веб-приложение
ASP.NET Core (модель — представление — контроллер)).
c. Нажмите кнопку Создать
Настройка стиля сайта
Выполните небольшую базовую настройку меню, макета и домашней страницы
сайта.
Откройте Views/Shared/_Layout.cshtml и внесите следующие изменения.
Измените каждое вхождение ContosoUniversity на Contoso University . Таких
элементов будет три.
Добавьте пункты меню About (Сведения), Students (Учащиеся), Courses
(Курсы), Instructors (Преподаватели) и Departments (Кафедры). Удалите пункт
меню Privacy.
Предыдущие изменения выделены в следующем коде:
CSHTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Contoso University</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbarlight bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home"
asp-action="Index">Contoso University</a>
<button class="navbar-toggler" type="button" datatoggle="collapse" data-target=".navbar-collapse" ariacontrols="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle
navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Home" asp-action="About">About</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Students" asp-action="Index">Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Courses" asp-action="Index">Courses</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Instructors" asp-action="Index">Instructors</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" aspcontroller="Departments" asp-action="Index">Departments</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2020 - Contoso University - <a asp-area="" aspcontroller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Замените содержимое файла Views/Home/Index.cshtml следующей разметкой:
CSHTML
@{
ViewData["Title"] = "Home Page";
}
<div class="jumbotron">
<h1>Contoso University</h1>
</div>
<div class="row">
<div class="col-md-4">
<h2>Welcome to Contoso University</h2>
<p>
Contoso University is a sample application that
demonstrates how to use Entity Framework Core in an
ASP.NET Core MVC web application.
</p>
</div>
<div class="col-md-4">
<h2>Build it from scratch</h2>
<p>You can build the application by following the steps in a series
of tutorials.</p>
<p><a class="btn btn-default"
href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the
tutorial »</a></p>
</div>
<div class="col-md-4">
<h2>Download it</h2>
<p>You can download the completed project from GitHub.</p>
<p><a class="btn btn-default"
href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef
-mvc/intro/samples/5cu-final">See project source code »</a></p>
</div>
</div>
Нажмите клавиши CTRL+F5, чтобы запустить проект, и выберите в меню Отладка >
Запуск без отладки. Домашняя страница отображается с вкладками для страниц,
созданных при работе с этим учебником.
EF Core Пакеты NuGet
В этом учебнике используется SQL Server, для которого требуется пакет поставщика
Microsoft.EntityFrameworkCore.SqlServer
.
Этот пакет EF SQL Server и его зависимости, Microsoft.EntityFrameworkCore и
Microsoft.EntityFrameworkCore.Relational , обеспечивают поддержку среды
выполнения для платформы EF.
Добавьте пакет NuGet Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore .
Введите в консоли диспетчера пакетов (PMC) следующие команды, чтобы добавить
пакеты NuGet:
PowerShell
Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Пакет Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet предоставляет
ASP.NET Core ПО промежуточного слоя для EF Core страниц ошибок. Это ПО
промежуточного слоя помогает обнаруживать и диагностировать ошибки при EF
Core миграции.
Сведения о других поставщиках баз данных, доступных для , см. в EF Coreразделе
Поставщики баз данных.
Создание модели данных
Для этого приложения будут созданы следующие классы сущностей:
У приведенных выше сущностей есть следующие отношения.
Между сущностями Student и Enrollment действует связь "один ко многим".
Учащегося можно зачислить на любое число курсов.
Между сущностями Course и Enrollment действует связь "один ко многим". На
курс может быть зачислено любое количество учащихся.
В следующих разделах создается класс для каждой из этих сущностей.
Сущность Student
В папке Models создайте класс Student , содержащий следующий код:
C#
using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Свойство ID используется в качестве столбца первичного ключа (ПК) в таблице
базы данных, соответствующей этому классу. По умолчанию платформа EF
интерпретирует в качестве первичного ключа свойство ID или classnameID .
Например, ПК может называться StudentID , а не ID .
Свойство Enrollments является свойством навигации. Свойства навигации
содержат другие сущности, связанные с этой сущностью. Свойство Enrollments
сущности Student :
Содержит все сущности Enrollment , связанные с этой сущностью Student .
Если определенная строка Student в базе данных содержит две связанные
строки Enrollment :
Свойство навигации Enrollments сущности Student содержит эти две
сущности Enrollment .
Строки Enrollment содержат значение первичного ключа учащегося в столбце
внешнего ключа (ВК) StudentID .
Если свойство навигации содержит несколько сущностей:
Тип должен быть списком, например ICollection<T> , List<T> или HashSet<T> .
Сущности можно добавлять, удалять и обновлять.
Связи "многие ко многим" и "один ко многим" могут содержать несколько
сущностей. Если используется ICollection<T> , платформа EF по умолчанию создает
коллекцию HashSet<T> .
Сущность Enrollment
В папке Models создайте класс Enrollment , содержащий следующий код:
C#
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
Свойство EnrollmentID — это ПК. Эта сущность использует шаблон classnameID
вместо самого по себе ID . Сущность Student использовала шаблон ID . Некоторые
разработчики предпочитают использовать один шаблон во всей модели данных. В
этом учебнике демонстрируется возможность использования любого из шаблонов.
В одном из следующих учебников показано, за счет чего использование ID без
имени класса позволяет упростить реализацию наследования в модели данных.
Свойство Grade имеет тип enum . Знак ? после объявления типа Grade указывает,
что свойство Grade допускает значение NULL. Оценка со значением null
отличается от нулевой оценки. При значении null оценка еще не известна или не
назначена.
Свойство StudentID представляет собой внешний ключ (ВК). Ему соответствует
свойство навигации Student . Сущность Enrollment связана с одной сущностью
Student , поэтому свойство содержит только отдельную сущность Student . Она
отличается от свойства навигации Student.Enrollments , которое содержит
несколько сущностей Enrollment .
Свойство CourseID представляет собой ВК. Ему соответствует свойство навигации
Course . Сущность Enrollment связана с одной сущностью Course .
Entity Framework интерпретирует свойство как свойство ВК, если оно называется
< имя_свойства_навигации >< имя_свойства_первичного_ключа > . Например,
StudentID для свойства навигации Student , так как сущность Student имеет
значение ПК ID . Свойства ВК также могут называться
< имя_свойства_первичного_ключа > . Например, CourseID , так как сущность Course
имеет значение ПК CourseID .
Сущность Course
В папке Models создайте класс Course , содержащий следующий код:
C#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Свойство Enrollments является свойством навигации. Сущность Course может быть
связана с любым числом сущностей Enrollment .
Атрибут DatabaseGenerated описан в следующем учебнике. Этот атрибут позволяет
ввести ПК для курса, а не использовать базу данных, чтобы создать его.
Создание контекста базы данных
Контекст базы данных DbContext — это основной класс, который координирует
функциональные возможности EF для определенной модели данных. Этот класс
является производным от класса Microsoft.EntityFrameworkCore.DbContext .
Производный класс DbContext указывает сущности, которые включаются в модель
данных. Некоторые расширения функциональности EF можно настроить. В этом
проекте соответствующий класс называется SchoolContext .
В папке проекта создайте папку Data .
В папке Data создайте файл SchoolContext , содержащий следующий код:
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
Представленный выше код создает свойство DbSet для каждого набора сущностей.
Терминология EF:
Набор сущностей обычно соответствует таблице базы данных.
Сущность соответствует строке в таблице.
Инструкции DbSet<Enrollment> и DbSet<Course> можно опустить. Это не нарушит
функциональность. EF включает их неявно, так как
сущность Student ссылается на сущность Enrollment ,
а сущность Enrollment ссылается на сущность Course .
При создании базы данных платформа EF создает таблицы с именами,
соответствующими именам свойств в DbSet . Имена свойств для коллекций обычно
указаны во множественном числе. Например, используйте идентификатор Students
вместо Student . В среде разработчиков нет единого мнения о том, следует ли
использовать имена таблиц во множественном числе. В этих учебниках вместо
принятого по умолчанию способа таблицам в DbContext присваиваются имена в
единственном числе. Чтобы сделать это, добавьте выделенный ниже код после
последнего свойства DbSet.
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
}
}
}
Зарегистрируйте SchoolContext .
ASP.NET Core поддерживает внедрение зависимостей. С помощью внедрения
зависимостей службы, например контекст базы данных EF, регистрируются во
время запуска приложения. Затем компоненты, которые используют эти службы,
например контроллеры MVC, обращаются к ним через параметры конструктора.
Код конструктора контроллера, который получает экземпляр контекста, будет
приведен позднее в этом учебнике.
Чтобы зарегистрировать SchoolContext как службу, откройте файл Startup.cs и
добавьте выделенные строки в метод ConfigureServices .
C#
using
using
using
using
using
using
using
ContosoUniversity.Data;
Microsoft.EntityFrameworkCore;
Microsoft.AspNetCore.Builder;
Microsoft.AspNetCore.Hosting;
Microsoft.Extensions.Configuration;
Microsoft.Extensions.DependencyInjection;
Microsoft.Extensions.Hosting;
namespace ContosoUniversity
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);
services.AddControllersWithViews();
}
Имя строки подключения передается в контекст путем вызова метода для объекта
DbContextOptionsBuilder . При локальной разработке система конфигурации
ASP.NET Core считывает строку подключения из файла appsettings.json .
Откройте файл appsettings.json и добавьте строку подключения, как показано в
следующей разметке:
JSON
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;
MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Добавление фильтра исключений базы данных
Добавьте AddDatabaseDeveloperPageExceptionFilter в ConfigureServices , как
показано в следующем коде:
C#
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
);
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddControllersWithViews();
}
AddDatabaseDeveloperPageExceptionFilter предоставляет полезные сведения об
ошибках в среде разработки.
SQL Server Express LocalDB
Строка подключения указывает базу данных SQL Server LocalDB. LocalDB — это
упрощенная версия ядра СУБД SQL Server Express, предназначенная для разработки
приложений и не ориентированная на использование в рабочей среде. LocalDB
запускается по запросу в пользовательском режиме, поэтому настройки не
слишком сложны. По умолчанию LocalDB создает файлы базы данных MDF в
каталоге C:/Users/<user> .
Инициализация базы данных с тестовыми
данными
EF создает пустую базу данных. В этом разделе добавляется метод, который
вызывается после создания базы данных и заполняет ее тестовыми данными.
Метод EnsureCreated будет использоваться для автоматического создания базы
данных. В одном из следующих учебников вы узнаете, как обрабатывать изменения
модели с использованием Code First Migrations, что позволяет изменять схему базы
данных вместо того, чтобы удалять и повторно создавать ее.
В папке Data создайте файл с именем DbInitializer , содержащий следующий код:
C#
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return;
// DB has been seeded
}
var students = new Student[]
{
new
Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.P
arse("2005-09-01")},
new
Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Pa
rse("2002-09-01")},
new
Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse
("2003-09-01")},
new
Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Pa
rse("2002-09-01")},
new
Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002
-09-01")},
new
Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Pars
e("2001-09-01")},
new
Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse
("2003-09-01")},
new
Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Pars
e("2005-09-01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var courses = new Course[]
{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}
Предыдущий код проверяет, существует ли база данных:
если база данных не найдена,
она создается и загружается вместе с тестовыми данными. Для повышения
производительности тестовые данные загружаются массивами, а не
коллекциями List<T> .
Если база данных найдена, никакие действия не выполняются.
Обновите Program.cs , включив в него следующий код.
C#
using
using
using
using
using
using
ContosoUniversity.Data;
Microsoft.Extensions.DependencyInjection;
Microsoft.AspNetCore.Hosting;
Microsoft.Extensions.Hosting;
Microsoft.Extensions.Logging;
System;
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
private static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>
();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger =
services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the
DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Файл Program.cs выполняет следующие действия при запуске приложения:
Получение экземпляра контекста базы данных из контейнера внедрения
зависимостей.
Вызовите метод DbInitializer.Initialize .
Удалите контекст по завершении работы метода Initialize , как показано в
следующем коде:
C#
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the
database.");
}
}
host.Run();
}
При первом запуске приложения создается база данных, которая заполняется
тестовыми данными. Каждый раз при изменении модели данных:
База данных удаляется.
Обновите метод заполнения и запустите заново с новой базой данных.
В последующих учебниках описывается, как изменить базу данных при изменении
модели данных, не прибегая к ее удалению и повторному созданию. При
изменении модели данных данные не теряются.
Создание контроллера и представлений
Используйте подсистему формирования шаблонов Visual Studio для добавления
контроллера и представлений MVC, которые будут использовать платформу EF для
запроса данных и их сохранения.
Автоматическое создание методов и представлений операций CRUD
(создание,
чтение, обновление и удаление) называется формированием шаблонов.
В обозревателе решений щелкните правой кнопкой мыши папку Controllers
и выберите Добавить > Создать шаблонный элемент.
В диалоговом окне Добавление шаблона:
Выберите Контроллер MVC с представлениями, использующий Entity
Framework.
Нажмите кнопку Добавить. Откроется диалоговое окно добавления
контроллера MVC с представлениями с использованием Entity Framework:
В разделе Класс модели выберите Student.
В разделе Класс контекста данных выберите SchoolContext.
Оставьте предлагаемое по умолчанию имя StudentsController.
Нажмите кнопку Добавить.
Подсистема формирования шаблонов Visual Studio создает файл
StudentsController.cs и набор представлений (файлы *.cshtml ), которые будут
работать с контроллером.
Обратите внимание, что контроллер принимает SchoolContext в качестве
параметра конструктора.
C#
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
public StudentsController(SchoolContext context)
{
_context = context;
}
Технология внедрения зависимостей ASP.NET Core обеспечивает передачу
экземпляра SchoolContext в контроллер. Вы настраиваете ее в классе Startup .
Контроллер содержит метод действия Index , который отображает всех учащихся в
базе данных. Этот метод получает список учащихся из набора сущностей Students,
считывая свойство Students экземпляра контекста базы данных:
C#
public async Task<IActionResult> Index()
{
return View(await _context.Students.ToListAsync());
}
Элементы асинхронного программирования в этом коде рассматриваются далее в
этом учебнике.
Представление Views/Students/Index.cshtml отображает этот список в таблице:
CSHTML
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a>
|
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Нажмите клавиши CTRL+F5, чтобы запустить проект, и выберите в меню Отладка >
Запуск без отладки.
Перейдите на вкладку Students (Учащиеся), чтобы просмотреть тестовые данные,
добавленные методом DbInitializer.Initialize . В зависимости от размеров окна
браузера ссылку на вкладку Students можно найти вверху страницы или щелкнув
значок навигации в правом верхнем углу.
Просмотр базы данных
При запуске приложения метод DbInitializer.Initialize вызывает EnsureCreated .
Платформа EF определила, что база данных отсутствует,
поэтому создала базу данных.
Код метода Initialize заполняет базу данных данными.
Для просмотра базы данных в Visual Studio используйте Обозреватель объектов
SQL Server (SSOX).
Выберите Обозреватель объектов SQL Server из меню Вид в Visual Studio.
В SSOX щелкните (localdb)\MSSQLLocalDB > Базы данных.
Выберите запись ContosoUniversity1 для имени базы данных, которая
находится в строке подключения в файле appsettings.json .
Разверните узел Таблицы, чтобы просмотреть представленные в базе
таблицы.
Щелкните правой кнопкой мыши таблицу Student и выберите пункт Просмотреть
данные, чтобы просмотреть данные в таблице.
Файлы базы данных *.mdf и *.ldf находятся в папке C:\Users\
<имя_пользователя>.
Так как EnsureCreated вызывается в методе инициализатора, который выполняется
при запуске приложения, можно сделать следующее:
Внести изменение в класс Student .
База данных удаляется.
Остановить, а затем запустить приложение. База данных автоматически
создается повторно в соответствии с изменением.
Например, при добавлении свойства EmailAddress в класс Student во вновь
созданной таблице появится новый столбец EmailAddress . В представлении не
будет отображаться новое свойство EmailAddress .
Соглашения
Объем кода, написанного для создания полной базы данных платформой EF,
сведен к минимуму благодаря использованию соглашений EF.
В качестве имен таблиц используются имена свойств DbSet . Для сущностей, на
которые не ссылается свойство DbSet , в качестве имен таблиц используются
имена классов сущностей.
В качестве имен столбцов используются имена свойств сущностей.
Свойства сущности с именами ID или classnameID распознаются как свойства
ПК.
Свойство интерпретируется как свойство ВК, если оно называется
< имя_свойства_навигации >< имя_свойства_первичного_ключа > . Например,
StudentID для свойства навигации Student , так как сущность Student имеет
значение ПК ID . Свойства ВК также могут называться
< имя_свойства_первичного_ключа > . Например, EnrollmentID , так как
сущность Enrollment имеет ПК EnrollmentID .
Стандартное поведение можно переопределить. Например, можно явно задать
имена таблиц, как было показано ранее в этом учебнике. Имена столбцов и любое
свойство можно задать в качестве ПК или ВК.
Асинхронный код
Асинхронное программирование — это режим по умолчанию для ASP.NET Core и
EF Core.
Веб-сервер имеет ограниченное число потоков, поэтому при высокой загрузке
могут использоваться все доступные потоки. В таких случаях сервер не может
обрабатывать новые запросы до тех пор, пока не будут высвобождены потоки. В
синхронном коде многие потоки могут быть заняты, не выполняя при этом какиелибо операции и ожидая завершения ввода-вывода. В асинхронном коде в то
время, когда процесс ожидает завершения ввода-вывода, его поток
высвобождается и может использоваться сервером для обработки других
запросов. Таким образом, асинхронный код позволяет более эффективно
использовать ресурсы сервера, который может обрабатывать больше трафика без
задержек.
Во время выполнения асинхронный код использует немного больше служебных
ресурсов, однако при низком объеме трафика этим можно пренебречь. Тем не
менее в случае большого объема трафика это дает существенный выигрыш в
производительности.
В следующем коде async , Task<T> , await и ToListAsync обеспечивают асинхронное
выполнение кода.
C#
public async Task<IActionResult> Index()
{
return View(await _context.Students.ToListAsync());
}
Ключевое слово async указывает компилятору создавать обратные вызовы
для частей тела метода и автоматически создавать возвращаемый объект
Task<IActionResult> .
Тип возвращаемого значения Task<IActionResult> представляет текущую
операцию с помощью результата типа IActionResult .
Ключевое слово await предписывает компилятору разделить метод на две
части. Первая часть завершается операцией, которая запускается в
асинхронном режиме. Вторая часть помещается в метод обратного вызова,
который вызывается при завершении операции.
ToListAsync является асинхронной версией метода расширения ToList .
При написании асинхронного кода, который использует EF, нужно учитывать
некоторые моменты:
Асинхронно выполняются только те инструкции, в результате которых в базу
данных отправляются запросы или команды. К ним относятся, например,
ToListAsync , SingleOrDefaultAsync и SaveChangesAsync . В их число не входят,
например, инструкции, которые просто изменяют IQueryable , такие как var
students = context.Students.Where(s => s.LastName == "Davolio") .
Контекст EF не является потокобезопасным, поэтому не следует пытаться
выполнять несколько операций параллельно. При вызове любого
асинхронного метода EF всегда используйте ключевое слово await .
Чтобы воспользоваться преимуществами в производительности, которые
обеспечивает асинхронный код, убедитесь, что все используемые пакеты
библиотек также используют асинхронный код при вызове любых методов EF,
выполняющих запросы к базе данных.
Дополнительные сведения об асинхронных методах программирования в .NET см.
в разделе Обзор асинхронной модели.
Ограничение полученных сущностей
Сведения об ограничении числа сущностей, возвращаемых в запросе, см. в статье
Важные замечания о производительности.
Ведение журнала SQL Entity Framework Core
Конфигурация ведения журналов обычно предоставляется разделом Logging в
файлах appsettings.{Environment}.json . Чтобы регистрировать инструкции SQL,
добавьте "Microsoft.EntityFrameworkCore.Database.Command": "Information" в файл
appsettings.Development.json :
JSON
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB2;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"AllowedHosts": "*"
}
При использовании приведенного выше кода JSON инструкции SQL отображаются
в командной строке и в окне вывода Visual Studio.
Дополнительные сведения см. в статье Ведение журнала в ASP.NET Core и
описании этой проблемы GitHub .
В следующем учебнике описано, как выполнять основные операции CRUD
(создание, чтение, обновление и удаление).
Реализация базовых функций CRUD
Руководство. Реализация
функциональных возможностей CRUD
— ASP.NET MVC с помощью EF Core
Статья • 28.01.2023 • Чтение занимает 17 мин
В предыдущем учебнике было создано приложение MVC, которое сохраняет и
отображает данные, используя платформу Entity Framework и SQL Server LocalDB. В
рамках этого учебника вы сможете ознакомиться с кодом операций CRUD
(создание, чтение, обновление, удаление), который автоматически создается
технологией формирования шаблонов MVC в контроллерах и представлениях, а
также настроить этот код.
7 Примечание
Широко распространена практика реализации шаблона репозитория,
позволяющего создать уровень абстракции между контроллером и уровнем
доступа к данным. Чтобы максимально упростить эти учебники и
сконцентрироваться на работе с самой платформой Entity Framework, мы не
используем в них репозитории. Дополнительные сведения о репозиториях на
платформе EF см. в последнем учебнике серии.
В этом учебнике рассмотрены следующие задачи.
" Настройка страницы сведений
" Обновление страницы Create
" Обновление страницы редактирования
" Обновление страницы удаления
" Закрытие подключений к базам данных
Предварительные требования
Начало работы с EF Core MVC и ASP.NET Core
Настройка страницы сведений
В шаблонном коде страницы указателя учащихся опущено свойство Enrollments ,
поскольку оно содержит коллекцию. На странице Details (Сведения) содержимое
коллекции отображается в таблице HTML.
В методе действия Controllers/StudentsController.cs для представления сведений
используется метод FirstOrDefaultAsync для извлечения одной сущности Student .
Добавьте код, который вызывает методы Include . ThenInclude и AsNoTracking , как
показано ниже в выделенном коде.
C#
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var student = await _context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}
return View(student);
}
Методы Include и ThenInclude инструктируют контекст для загрузки свойства
навигации Student.Enrollments , а также свойства навигации Enrollment.Course в
пределах каждой регистрации. Дополнительные сведения об этих методах см. в
учебнике Чтение связанных данных.
Метод AsNoTracking повышает производительность в тех сценариях, где
возвращаемые сущности не будут обновляться во время существования текущего
контекста. Дополнительные сведения о методе AsNoTracking приводятся в конце
этого учебника.
Данные маршрута
Значение ключа, которое передается в метод Details , поступает из данных
маршрута. Данные маршрута обнаруживаются связывателем модели в сегменте
URL-адреса. Например, маршрут по умолчанию задает сегменты контроллера,
действия и идентификатора:
C#
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
В следующем URL-адресе маршрут по умолчанию сопоставляет контроллер
Instructor, действие Index и идентификатор 1, которые принимаются в качестве
значений данных маршрута.
http://localhost:1230/Instructor/Index/1?courseID=2021
Последняя часть URL-адреса ("?courseID=2021") представляет значение строки
запроса. Связыватель модели также будет передавать значение идентификатора в
метод Index в параметр id , если вы передаете его в качестве значения строки
запроса:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
На странице Index URL-адреса гиперссылок создаются с помощью инструкций
вспомогательной функции тегов в представлении Razor. В следующем коде Razor
параметр id соответствует маршруту по умолчанию, поэтому к данным маршрута
добавляется id .
HTML
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>
Если item.ID равен 6, создается следующий код HTML:
HTML
<a href="/Students/Edit/6">Edit</a>
В следующем коде Razor studentID не соответствует параметру в маршруте по
умолчанию и добавляется в качестве строки запроса.
HTML
<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>
Если item.ID равен 6, создается следующий код HTML:
HTML
<a href="/Students/Edit?studentID=6">Edit</a>
Дополнительные сведения о вспомогательных функциях тегов см. в разделе
Вспомогательные функции тегов в ASP.NET Core.
Добавление регистраций в представление сведений
Откройте Views/Students/Details.cshtml . Каждое поле отображается с помощью
вспомогательных функций DisplayNameFor и DisplayFor , как показано в следующем
примере:
CSHTML
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.LastName)
</dd>
После последнего поля и непосредственно перед закрывающим тегом </dl>
добавьте следующий код, чтобы отобразить список регистраций:
CSHTML
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Enrollments)
</dt>
<dd class="col-sm-10">
<table class="table">
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</dd>
Если после вставки кода нарушаются отступы в нем, нажмите клавиши CTRL-K-D,
чтобы исправить это.
Этот код циклически обрабатывает сущности в свойстве навигации Enrollments .
Для каждой регистрации он отображает название курса и оценку. Название курса
извлекается из сущности Course, которая хранится в свойстве навигации Course
сущности Enrollments.
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните ссылку
Details (Сведения) для учащегося. Откроется список курсов и оценок для
выбранного учащегося:
Обновление страницы Create
В файле StudentsController.cs измените метод HttpPost Create , добавив в него
блок try-catch и удалив идентификатор из атрибута Bind .
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}
Этот код добавляет сущность Student, созданную связывателем модели ASP.NET
Core MVC, в набор сущностей Students, после чего сохраняет изменения в базе
данных. (Связыватель модели использует функциональные возможности ASP.NET
Core MVC для упрощения работы с данными, предоставляемыми в форме. Он
преобразует значения из отправленной формы в типы CLR и передает их в виде
параметров в метод действия. В нашем случае связыватель создает экземпляр
сущности Student, используя значения свойств из коллекции Form.)
ID удаляется из атрибута Bind в связи с тем, что он содержит значение первичного
ключа, которое будет автоматически устанавливаться SQL Server при вставке
строки. Значение ID не задается на основе введенных пользователем данных.
Помимо атрибута Bind , в шаблонном коде изменяется только блок try-catch. Если
во время сохранения изменений перехватывается исключение, производное от
DbUpdateException , отображается сообщение об общей ошибке. Исключения
DbUpdateException иногда связаны с внешними факторами, а не с ошибкой при
программировании приложения, поэтому рекомендуется попробовать повторить
выполненные действия снова. В этом примере такое поведение не реализовано,
однако в рабочем приложении, как правило, исключения заносятся в журнал.
Дополнительные сведения см. в разделе Ведение журналов для анализа статьи
Мониторинг и телеметрия (построение реальных облачных приложений для Azure).
Атрибут ValidateAntiForgeryToken позволяет предотвратить атаки с подделкой
межсайтовых запросов. Токен автоматически вставляется в представление с
помощью FormTagHelper и включается при отправке формы пользователем. Токен
проверяется по атрибуту ValidateAntiForgeryToken . Дополнительные сведения см.
на странице Предотвращение атак с использованием подделки межсайтовых
запросов (XSRF/CSRF) в ASP.NET Core.
Примечание о безопасности в связи с атаками
чрезмерной передачи данных
Атрибут Bind , который включается шаблонным кодом в метод Create , является
одним из способов защиты от чрезмерной передачи данных в сценариях создания.
Допустим, сущность Student включает свойство Secret , которое не требуется
устанавливать на этой веб-странице.
C#
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public string Secret { get; set; }
}
Даже если на веб-странице отсутствует поле Secret , злоумышленник может
использовать такие средства, как Fiddler, или собственный код JavaScript, для
отправки значения формы Secret . Если отсутствует атрибут Bind , ограничивающий
поля, которые связыватель модели использует при создании экземпляра Student,
связыватель модели выберет это значение формы Secret и использует его для
создания экземпляра сущности Student. Таким образом, какое бы значение ни
задал злоумышленник для поля Secret , оно будет обновлено в базе данных. На
следующем рисунке показано средство Fiddler, с помощью которого в
отправленные значения формы добавляется поле Secret (со значением
"OverPost").
После этого значение "OverPost" будет успешно добавлено в свойство Secret
вставленной строки, хотя вы не разрешали установку этого свойства на вебстранице.
Чтобы предотвратить чрезмерную передачу данных в сценариях редактирования,
можно сначала считать сущность из базы данных и затем вызвать метод
TryUpdateModel , передав в него список явно разрешенных свойств. Именно этот
метод используется в этих учебниках.
Кроме того, многие разработчики предпочитают для защиты от чрезмерной
передачи данных использовать модели представлений вместо классов сущностей с
привязкой моделей. Включайте только те свойства, которые требуется обновлять в
модели представления. После завершения работы связывателя модели MVC
скопируйте свойства модели представления в экземпляр сущности, например с
помощью такого средства, как AutoMapper. С помощью _context.Entry в
экземпляре сущности задайте для него состояние Unchanged , после чего присвойте
значение true атрибуту Property("PropertyName").IsModified для каждого свойства,
которое включается в модель представления. Этот метод подходит для сценариев
редактирования и создания.
Проверка страницы создания
Для каждого поля в коде в Views/Students/Create.cshtml используются
вспомогательные функции тегов label , input и span (для сообщений о проверке).
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните Create
New (Создать).
Введите имена и даты. Если браузер допускает это, попробуйте ввести
недопустимую дату. (В некоторых браузерах принудительно используется
управляющий элемент выбора даты.) Щелкните Create (Создать), чтобы
просмотреть сообщение об ошибке.
Эта проверка по умолчанию выполняется на стороне сервера. Позднее в учебнике
вы узнаете, как добавлять атрибуты, которые будут создавать код для проверки на
стороне клиента. В выделенном ниже коде демонстрируется проверка модели в
методе Create .
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
[Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
{
try
{
if (ModelState.IsValid)
{
_context.Add(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists " +
"see your system administrator.");
}
return View(student);
}
Измените дату на допустимую и щелкните Create (Создать), чтобы добавить нового
учащегося на страницу Index (Указатель).
Обновление страницы редактирования
В файле StudentController.cs метод HttpGet Edit (метод без атрибута HttpPost )
использует метод FirstOrDefaultAsync для извлечения выбранной сущности
учащегося, как показано в методе Details . Изменять этот метод не нужно.
Рекомендуемый код метода HttpPost Edit: чтение и
изменение
Замените метод действия HttpPost Edit следующим кодом.
C#
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s =>
s.ID == id);
if (await TryUpdateModelAsync<Student>(
studentToUpdate,
"",
s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(studentToUpdate);
}
Благодаря этому изменению реализуются рекомендации по безопасности,
позволяющие предотвратить чрезмерную отправку данных. Шаблон создал
атрибут Bind и добавил сущность, созданную связывателем модели, в набор
сущностей с флагом Modified . В большинстве сценариев не рекомендуется
использовать этот код, поскольку атрибут Bind очищает любые ранее
существовавшие данные в полях, которые не перечислены в параметре Include .
Новый код считывает существующую сущность и вызывает метод TryUpdateModel
для обновления полей в извлеченной сущности на основании данных, введенных
пользователем в отправленной форме. Технология автоматического отслеживания
изменений платформы Entity Framework устанавливает флаг Modified для полей,
которые были изменены на основе введенных в форму данных. При вызове метода
SaveChanges платформа Entity Framework создает инструкции SQL для обновления
строки базы данных. Конфликты параллелизма игнорируются, а в базе данных
обновляются только те столбцы таблицы, которые были обновлены пользователем.
(Порядок обработки конфликтов параллелизма будет показан позднее в учебнике.)
Чтобы предотвратить чрезмерную передачу данных, рекомендуется объявить поля,
которые требуется обновлять на странице Изменение, в параметрах
TryUpdateModel . (Пустая строка перед списком полей в списке параметров
предназначена для префикса, который используется с именами полей формы.) На
данный момент другие поля не защищаются. Если включить в список поля,
которые должен привязывать связыватель модели, это позволяет гарантировать,
что при добавлении полей в модель данных в будущем они будут автоматически
защищаться до тех пор, пока вы явно не добавите их сюда.
В результате этих изменений сигнатура метода HttpPost Edit будет совпадать с
методом HttpGet Edit . Таким образом, вы просто переименовали метод EditPost .
Альтернативный код метода HttpPost Edit: создание и
подключение
Рекомендуемый код метода HttpPost гарантирует обновление только измененных
столбцов и сохраняет данные в свойствах, которые не требуется включать в
привязку моделей. Тем не менее при подходе с предварительным считыванием
дополнительно выполняется чтение из базы данных, в результате чего код может
усложняться для обработки конфликтов параллелизма. В качестве альтернативы
можно присоединить сущность, созданную связывателем модели, к контексту EF и
пометить ее как измененную. (Не добавляйте этот код в проект, поскольку он
показан исключительно как пример альтернативного подхода.)
C#
public async Task<IActionResult> Edit(int id,
[Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
{
if (id != student.ID)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
}
return View(student);
}
Этот подход можно использовать в тех случаях, когда пользовательский интерфейс
веб-страницы включает все поля сущности и может обновлять любые из них.
Шаблонный код используется подход с созданием и присоединением, однако лишь
перехватывает исключения DbUpdateConcurrencyException и возвращает коды
ошибок 404. В показанном примере перехватываются любые исключения
обновления базы данных и отображается сообщение об ошибке.
Состояния сущностей
Контекст базы данных отслеживает состояние синхронизации сущностей в памяти с
соответствующими им строками в базе данных. Данные отслеживания определяют,
что происходит при вызове метода SaveChanges . Например, при передаче новой
сущности в метод Add ей присваивается состояние Added . При последующем
вызове метода SaveChanges контекст базы данных выполняет команду SQL INSERT.
Возможны следующие состояния сущности:
Added . Сущность еще не существует в базе данных. Метод SaveChanges
выполняет инструкцию INSERT.
Unchanged . С этой сущностью не нужно выполнять никакие действия с
помощью метода SaveChanges . Это начальный статус сущности, который она
имеет при чтении из базы данных.
Modified . Были изменены значения некоторых или всех свойств сущности.
Метод SaveChanges выполняет инструкцию UPDATE.
Deleted . Сущность отмечена для удаления. Метод SaveChanges выполняет
инструкцию DELETE.
Detached . Сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются
автоматически. Например, вы можете считать сущность и изменить значения
некоторых ее свойств. В этом случае состояние сущности автоматически изменится
на Modified . Если затем вызвать метод SaveChanges , платформа Entity Framework
выполнит инструкцию SQL UPDATE, которая обновит только фактически
измененные свойства.
В веб-приложении объект DbContext , который изначально считывает сущность и
отображает ее данные для редактирования, ликвидируется после отрисовки
страницы. При вызове метода действия HttpPost Edit выполняется новый веб-
запрос и создается новый экземпляр DbContext . Если повторно считать сущность в
этот новый контекст, таким образом будет смоделирована обработка в
классическом приложении.
Однако если выполнять дополнительную операцию чтения не требуется,
необходимо использовать объект сущности, созданный связывателем модели. Для
этого проще всего присвоить сущности состояние Modified, как это сделано в
показанном ранее альтернативном методе HttpPost Edit. При последующем вызове
метода SaveChanges платформа Entity Framework обновляет все столбцы в строке
базы данных, поскольку у контекста нет возможности определить, какие свойства
были изменены.
Если вы не хотите сначала выполнять чтение, но вам нужно, чтобы инструкция SQL
UPDATE обновляла только те поля, которые пользователь фактически изменяет, код
будет выглядеть более сложным. Вам необходимо каким-либо образом сохранить
исходные значения (например, используя скрытые поля), чтобы они были доступны
при вызове метода HttpPost Edit . Затем вы можете создать сущность Student,
используя исходные значения, вызвать метод Attach с исходной версией этой
сущности, обновить значения сущности и вызвать метод SaveChanges .
Проверка страницы редактирования
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните
гиперссылку Edit (Изменить).
Измените определенные данные и нажмите кнопку Save (Сохранить). Откроется
страница Index (Указатель), на которой будут представлены измененные данные.
Обновление страницы удаления
В файле StudentController.cs в коде шаблона для метода HttpGet Delete
используется метод FirstOrDefaultAsync для извлечения выбранной сущности
учащегося, как показано в методах сведений и редактирования. Тем не менее,
чтобы реализовать настраиваемое сообщение об ошибке при сбое вызова метода
SaveChanges , необходимо добавить некоторые функции в этот метод и
соответствующее ему представление.
Как и в случае с операциями обновления и создания, операции удаления требуют
двух методов действия. Метод, вызываемый в ответ на запрос GET, отображает
представление, в котором пользователь может подтвердить или отменить
операцию удаления. Если пользователь подтверждает ее, создается запрос POST. В
этом случае вызывается метод HttpPost Delete , который фактически выполняет
операцию удаления.
Для обработки ошибок, которые могут произойти при обновлении базы данных,
следует добавить в метод HttpPost Delete блок try-catch. В случае ошибки метод
HttpPost Delete вызывает метод HttpGet Delete, передавая в него параметр,
указывающий на состояние ошибки. Метод HttpGet Delete повторно отображает
страницу подтверждения и сообщение об ошибке, предлагая пользователю
отменить операцию или повторить ее еще раз.
Замените метод действия HttpGet Delete следующим кодом, в котором
реализуется управление сообщениями об ошибках.
C#
public async Task<IActionResult> Delete(int? id, bool? saveChangesError =
false)
{
if (id == null)
{
return NotFound();
}
var student = await _context.Students
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (student == null)
{
return NotFound();
}
if (saveChangesError.GetValueOrDefault())
{
ViewData["ErrorMessage"] =
"Delete failed. Try again, and if the problem persists " +
"see your system administrator.";
}
return View(student);
}
Этот код принимает необязательный параметр, который указывает, был ли метод
вызван после сбоя при сохранении изменений. Если перед вызовом метода
HttpGet Delete не произошел сбой, этот параметр будет иметь значение false. Если
он вызывается методом HttpPost Delete в ответ на ошибку при обновлении базы
данных, этот параметр будет иметь значение true, а в представление передается
сообщение об ошибке.
Подход с предварительным чтением для метода
HttpPost Delete
Замените метод действия HttpPost Delete (имеет имя DeleteConfirmed ) следующим
кодом, в котором выполняется фактическая операция удаления и перехватываются
любые ошибки при обновлении базы данных.
C#
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var student = await _context.Students.FindAsync(id);
if (student == null)
{
return RedirectToAction(nameof(Index));
}
try
{
_context.Students.Remove(student);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id,
saveChangesError = true });
}
}
Этот код извлекает выбранную сущность и вызывает метод Remove , чтобы
присвоить ей состояние Deleted . При вызове метода SaveChanges создается
инструкция SQL DELETE.
Подход с созданием и присоединением для метода
HttpPost Delete
Если требуется обеспечить максимальную производительность крупного
приложения, можно избежать создания ненужных запросов SQL. Для этого можно
создать экземпляр сущности Student, используя только значение первичного
ключа, и затем присвоить этой сущности состояние Deleted . Это все, что
платформе Entity Framework необходимо для удаления сущности. (Не используйте
этот код в проекте. Он показан здесь исключительно в качестве примера.)
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
try
{
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { id = id,
saveChangesError = true });
}
}
Если также требуется удалить связанные с сущностью данные, убедитесь, что в базе
данных настроено каскадное удаление. При таком подходе к удалению сущности
платформе EF может быть неизвестно о наличии связанных сущностей, которые
требуется удалить.
Обновление представления удаления
В файле Views/Student/Delete.cshtml добавьте сообщение об ошибке между
заголовками h2 и h3, как показано в следующем примере:
CSHTML
<h2>Delete</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>
Запустите приложение, выберите вкладку Students (Учащиеся) и щелкните
гиперссылку Delete (Удалить):
Щелкните Delete (Удалить). Отображается страница Index (Указатель), на которой
удаленный учащийся будет отсутствовать. (В учебнике, посвященном
параллелизму, приводится пример кода обработки ошибок.)
Закрытие подключений к базам данных
Чтобы высвободить ресурсы, используемые подключением к базе данных,
необходимо как можно скорее ликвидировать экземпляр контекста после
завершения работы с ним. Эта задача реализуется с помощью встроенной в
ASP.NET Core технологии внедрения зависимостей.
В Startup.cs вызывается метод расширения AddDbContext , чтобы подготовить
класс DbContext в контейнере DI ASP.NET Core. Этот метод по умолчанию
устанавливает время существования службы Scoped . Значение Scoped указывает,
что срок существования объекта контекста соответствует сроку существования вебзапроса. Таким образом, по завершении веб-запроса автоматически будет
вызываться метод Dispose .
Обработка транзакций
По умолчанию платформа Entity Framework реализует транзакции неявно. В
сценариях, когда вы вносите изменения в несколько строк или таблиц и затем
вызываете метод SaveChanges , платформа Entity Framework автоматически
гарантирует, что одновременно все изменения либо выполняются успешно, либо
завершаются неудачно. Если ошибка происходит после того, как были выполнены
некоторые изменения, эти изменения автоматически откатываются. Если вам
требуется дополнительный контроль, например в сценариях с операциями,
выполняемыми в транзакции вне платформы Entity Framework, ознакомьтесь с
разделом Транзакции.
Отключение отслеживания запросов
Когда контекст базы данных извлекает строки таблицы и создает представляющие
их объекты сущностей, по умолчанию отслеживается состояние синхронизации
сущностей в памяти с содержимым базы данных. При обновлении сущности
данные в памяти выступают в роли кэша. В веб-приложении такое кэширование
часто не нужно, поскольку экземпляры контекста, как правило, существуют недолго
(для каждого запроса создается и ликвидируется собственный экземпляр), и
контекст, считывающий сущность, как правило, ликвидируется до того, как
сущность будет использована снова.
Чтобы отключить отслеживание объектов сущностей в памяти, вызовите метод
AsNoTracking . Как правило, это требуется в следующих сценариях:
В течение срока существования контекста не требуется обновлять сущности, и
не нужно, чтобы платформа EF автоматически загружала свойства навигации
с сущностями, извлекаемыми с помощью отдельных запросов. Эти условия
часто выполняются в методах действия контроллера HttpGet.
Выполняется запрос, который извлекает большой объем данных, и при этом
обновляется только небольшая часть возвращаемых данных. Для повышения
эффективности можно отключить отслеживание для больших запросов и
выполнить запрос позднее для нескольких обновляемых сущностей.
Необходимо присоединить запрос для его обновления, однако ранее та же
сущность уже была извлечена для других целей. Поскольку сущность уже
отслеживается контекстом базы данных, присоединить сущность, которую
требуется изменить, нельзя. Одним из решений в такой ситуации является
вызов метода AsNoTracking для предшествующего запроса.
Дополнительные сведения см. в разделе Работа с отслеживанием и и без него.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Настройка страницы сведений
" Обновление страницы создания
" Обновление страницы редактирования
" Обновление страницы удаления
" Закрытие подключений к базам данных
В следующем учебнике описано, как добавить на страницу Index (Указатель)
функции добавления, сортировки, фильтрации и разбиения на страницы.
Далее: Сортировка, фильтрация и разбиение на страницы
Руководство по добавлению
сортировки, фильтрации и разбиения
по страницам — ASP.NET MVC с EF
Core
Статья • 28.01.2023 • Чтение занимает 12 мин
В предыдущем руководстве был создан набор веб-страниц для основных операций
CRUD для сущностей Student. Из этого руководства вы узнаете, как добавить на
страницу указателя учащихся сортировку, фильтрацию и разбиение на страницы.
Здесь также описывается создание страницы с простой группировкой.
На следующем рисунке изображен вид страницы после выполнения задания.
Заголовки столбцов являются ссылками, при нажатии на которые происходит
сортировка по этому столбцу. При повторном нажатии на заголовок происходит
переключение между сортировкой по возрастанию и убыванию.
В этом учебнике рассмотрены следующие задачи.
" Добавление ссылок для сортировки столбцов
" Добавление поля поиска
" Добавление разбиения по страницам в указатель учащихся
" Добавление разбиения по страницам в метод Index
" Добавление ссылок для разбиения по страницам
" Создание страницы сведений
Предварительные требования
Реализация функциональности CRUD
Добавление ссылок для сортировки
столбцов
Для добавления сортировки на страницу указателя учащихся изменим метод Index
контроллера Students и добавим код в представление указателя учащихся.
Добавление сортировки в метод Index
В StudentsController.cs замените метод Index следующим кодом:
C#
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Этот код принимает параметр sortOrder из строки запроса в URL. Значение строки
запроса в ASP.NET Core MVC передается как параметр метода действия. Имя
параметра представляет собой строку, состоящую из "Name" или "Date" с
возможным добавлением знака подчеркивания и строки "desc" для указания
убывающего порядка сортировки. По умолчанию используется порядок
сортировки по возрастанию.
При первом запросе страницы Index строка запроса отсутствует. Список студентов
отсортирован по фамилиям по возрастанию, порядок сортировки по умолчанию
задается в выражении switch . Когда пользователь щелкает гиперссылку заголовка
столбца, в строку запроса подставляется соответствующее значение параметра
sortOrder .
Для формирования гиперссылок в заголовках столбцов в представлении
используются два элемента ViewData (NameSortParm и DateSortParm) с
соответствующими значениями строки запроса.
C#
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Это тернарные условные операторы. Первое выражение означает, что если
параметр sortOrder пуст или равен null, то параметр NameSortParm должен
принять значение "name_desc", в противном случае параметру NameSortParm
присваивается пустая строка. Следующие два оператора устанавливают
гиперссылки в заголовках столбцов в представлении следующим образом:
Текущий порядок сортировки
Гиперссылка "Last Name"
(Фамилия)
Гиперссылка "Date"
(Дата)
"Last Name" (Фамилия) по
возрастанию
descending
ascending
"Last Name" (Фамилия) по
убыванию
ascending
ascending
"Date" (Дата) по возрастанию
ascending
descending
"Date" (Дата) по убыванию
ascending
ascending
Для указания столбца, по которому выполняется сортировка, этот метод использует
LINQ to Entities. Данный код создает переменную IQueryable перед оператором
switch, изменяет ее значение внутри оператора switch и вызывает метод
ToListAsync после switch . После создания и изменения переменных IQueryable
запрос в базу данных не отправляется. Запрос не выполнится, пока вы не
преобразуете объект IQueryable в коллекцию, вызвав соответствующий метод,
такой как ToListAsync . Таким образом, этот код создает одиночный запрос,
который не будет выполнен до выполнения выражения return View .
Этот код можно расширить на случай большого числа столбцов. В последнем
руководстве серии вы найдете пример кода, который позволяет передавать имя
столбца OrderBy в строковой переменной.
Добавление гиперссылок для заголовков столбцов в
представлении индекса учащихся
Для добавления гиперссылок в заголовки столбцов замените код в файле
Views/Students/Index.cshtml следующим кодом. Измененные строки выделены.
CSHTML
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-routesortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model =>
model.LastName)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-routesortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model =>
model.EnrollmentDate)</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a>
|
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Для формирования гиперссылок с соответствующей строкой запроса этот код
использует информацию из свойств ViewData .
Для проверки работы сортировки запустите приложение, выберите вкладку
Students и нажимайте на заголовки столбцов Last Name и Enrollment Date.
Добавление поля поиска
Для добавления фильтра на страницу указателя учащихся необходимо добавить в
представление текстовое поле и кнопку отправки и внести изменения в метод
Index . Текстовое поле необходимо для ввода строки для поиска в полях имени и
фамилии.
Добавление функций фильтрации в метод Index
В файле StudentsController.cs замените метод Index следующим кодом
(изменения выделены).
C#
public async Task<IActionResult> Index(string sortOrder, string
searchString)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Мы добавили в метод Index параметр searchString . Значение строки поиска
получается из текстового поля, которое мы добавили в представление Index. Мы
также добавили в запрос LINQ предложение where, которое отбирает только
студентов, чье имя или фамилия содержат строку поиска. Выражение с
предложением where выполняется только в том случае, если задано значение для
поиска.
7 Примечание
В этом коде мы вызываем метод Where объекта IQueryable , при этом фильтр
будет обработан на сервере. В некоторых случаях может потребоваться вызов
метода Where как метода расширения коллекции в памяти. (Предположим,
например, что вы изменили ссылку на _context.Students таким образом, что
вместо объекта EF DbSet она ссылается на метод репозитория, который
возвращает коллекцию IEnumerable .) Обычно результат остается прежним, но
в некоторых случаях он может отличаться.
Например, в .NET Framework метод Contains по умолчанию выполняет
сравнение с учетом регистра, а в SQL Server это определяется параметром
сортировки конкретного экземпляра SQL сервера. По умолчанию параметр
установлен на сравнение без учета регистра. Можно вызвать метод ToUpper ,
чтобы сделать сравнение явно регистронезависимым: Where(s =>
s.LastName.ToUpper().Contains(searchString.ToUpper()). Это гарантирует, что
поведение программы не изменится, если вы измените код на использование
репозитория, который возвращает IEnumerable , а не объект IQueryable . (При
вызове метода Contains коллекции IEnumerable выполняется реализация .NET
Framework; при вызове этого же метода у объекта IQueryable выполняется
реализация поставщика базы данных.) Однако такое решение снижает
производительность. Метод ToUpper добавляет функцию в предложение
WHERE TSQL-выражения SELECT. Это не позволяет оптимизатору использовать
индекс. Учитывая, что SQL обычно настраивается на то, чтобы не учитывать
регистр, рекомендуется не использовать код ToUpper до миграции на
хранилище данных, учитывающее регистр.
Добавление поля поиска на страницу индекса
учащихся
В файле Views/Student/Index.cshtml добавьте выделенный код непосредственно
перед открывающим тегом table для создания заголовка, текстового поля и кнопки
Search.
CSHTML
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
<table class="table">
Для добавления кнопки и поля поиска этот код использует вспомогательную
функцию тега <form> . По умолчанию вспомогательная функция тега <form>
отправляет данные формы с помощью запроса POST, это означает, что параметры
передаются в теле сообщения HTTP, а не в URL-адресе в виде строки запросов. При
указании метода HTTP GET данные формы передаются в URL-адресе в виде строк
запроса, что позволяет добавлять URL-адреса в закладки. Руководства
консорциума W3C рекомендуют использовать метод GET, когда действие не
приводит к обновлению.
Для проверки работы фильтра запустите приложение, выберите вкладку Students,
введите строку поиска и нажмите Search.
Обратите внимание, что URL-адрес содержит строку поиска.
HTML
http://localhost:5813/Students?SearchString=an
Если вы добавите эту страницу в закладки, то при открытии закладки будет
открываться уже отфильтрованный список. Формирование строки запроса
обеспечивает добавление method="get" в тег form .
На данном этапе, если нажать ссылку сортировки в заголовке столбца, то значение
фильтра, которое мы ввели в поле Search, будет потеряно. Мы исправим это в
следующем разделе.
Добавление разбиения по страницам в
указатель учащихся
Чтобы добавить на страницу указателя учащихся разбиение на страницы, следует
создать класс PaginatedList , который использует операторы Skip и Take для
фильтрации данных на сервере вместо того, чтобы каждый раз получать все строки
таблицы. Затем мы внесем дополнительные изменения в метод Index и добавим в
представление Index кнопки перелистывания страниц. На следующем рисунке
показаны кнопки перелистывания.
В папке проекта создайте файл PaginatedList.cs и замените код шаблона на
следующий код.
C#
using
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
Microsoft.EntityFrameworkCore;
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int
pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;
public bool HasNextPage => PageIndex < TotalPages;
public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T>
source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) *
pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
В этом коде метод CreateAsync принимает размер и номер страницы и вызывает
соответствующие методы Skip и Take объекта IQueryable . Метод ToListAsync
объекта IQueryable при вызове возвратит список, содержащий только
запрошенную страницу. Для включения и отключения кнопок перелистывания
страниц Previous и Next можно использовать свойства HasPreviousPage и
HasNextPage .
Для создания объекта PaginatedList<T> вместо конструктора используется метод
CreateAsync , поскольку конструкторы не могут выполнять асинхронный код.
Добавление разбиения по страницам в
метод Index
В StudentsController.cs замените метод Index следующим кодом:
C#
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc"
: "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1,
pageSize));
}
Этот код добавляет к сигнатуре метода параметры с номером страницы, текущим
порядком сортировки и текущим фильтром.
C#
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
При первом отображении страницы или если пользователь еще не нажимал на
ссылки сортировки и перелистывания, все параметры будут иметь значение null.
При нажатии на кнопку перелистывания переменная page будет содержать номер
страницы для отображения.
Элемент ViewData с именем CurrentSort передает в представление порядок
сортировки, поскольку он должен быть включен в ссылки перелистывания, чтобы
сохранить порядок сортировки при переходе по страницам.
Элемент ViewData с именем CurrentFilter передает в представление текущую строку
фильтра. Это значение необходимо включить в ссылки для перелистывания, чтобы
при смене страницы сохранить настройки фильтра, кроме того, необходимо
восстановить значение фильтра в текстовом поле после обновления страницы.
Если строка поиска изменяется во время перелистывания, то номер страницы
должен быть сброшен на 1, так как с новым фильтром изменится состав
отображаемых данных. Изменение строки поиска происходит при вводе в
текстовое поле значения и нажатии на кнопку отправки. В этом случае значение
параметра searchString не null.
C#
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
В конце метода Index метод PaginatedList.CreateAsync преобразует результат
запроса студентов в страницу коллекции, поддерживающую разбиение на
страницы. Это страница со студентами затем передается в представление.
C#
return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1,
pageSize));
Метод PaginatedList.CreateAsync принимает номер страницы. Два вопросительных
знака являются оператором объединения с null. Этот оператор определяет
значение по умолчанию для значения null; выражение (pageNumber ?? 1)
возвращает значение переменной pageNumber , если она имеет значение, и
возвращает 1, если переменная pageNumber имеет значение null.
Добавление ссылок для разбиения по
страницам
В Views/Students/Index.cshtml замените существующий код следующим кодом:
Изменения выделены.
CSHTML
@model PaginatedList<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString"
value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-routesortOrder="@ViewData["NameSortParm"]" asp-routecurrentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-routesortOrder="@ViewData["DateSortParm"]" asp-routecurrentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-routeid="@item.ID">Details</a> |
<a asp-action="Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>
Оператор @model в начале страницы указывает на то, что теперь представление
принимает объект PaginatedList<T> , а не объект List<T> .
Ссылки в заголовках столбцов передают в контроллер при помощи строки запроса
текущее значение строки поиска, чтобы пользователь мог сортировать
отфильтрованные данные:
HTML
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asproute-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
Кнопки перелистывания отображаются вспомогательными функциями тегов:
HTML
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
Запустите приложение и перейдите на страницу Students.
Чтобы убедиться, что постраничный просмотр работает, нажимайте кнопки
перелистывания при различном порядке сортировки. Затем введите строку поиска
и повторите перелистывание, чтобы убедиться, что разбиение на страницы
работает корректно вместе с сортировкой и фильтрацией.
Создание страницы сведений
На странице About веб-сайта "Университет Contoso" будет отображаться
количество зачисленных студентов по дням. Для этого понадобится группировка и
выполнение простых расчетов в группах. Для выполнения этой задачи нам
потребуется следующее:
Создать класс модели представления для данных, которые необходимо
передать в представление.
Создать метод About в контроллере Home.
Создать представление About.
Создание модели представления
Создайте папку SchoolViewModels в папке Models.
В новой папке добавьте файл класса EnrollmentDateGroup.cs и замените код
шаблона следующим кодом:
C#
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
Изменение контроллера Home
В HomeController.cs добавьте следующие операторы Using в верхнюю часть файла:
C#
using
using
using
using
Microsoft.EntityFrameworkCore;
ContosoUniversity.Data;
ContosoUniversity.Models.SchoolViewModels;
Microsoft.Extensions.Logging;
Добавьте переменную класса для контекста базы данных сразу же после
открывающей фигурной скобки описания класса и получите экземпляр контекста
из ASP.NET Core DI:
C#
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly SchoolContext _context;
public HomeController(ILogger<HomeController> logger, SchoolContext
context)
{
_logger = logger;
_context = context;
}
Добавьте метод About со следующим кодом.
C#
public async Task<ActionResult> About()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(await data.AsNoTracking().ToListAsync());
}
Запрос LINQ группирует записи из таблицы студентов по дате зачисления,
вычисляет число записей в каждой группе и сохраняет результаты в коллекцию
объектов моделей представления EnrollmentDateGroup .
Создание представления About
Добавьте файл Views/Home/About.cshtml с помощью следующего кода:
CSHTML
@model
IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
ViewData["Title"] = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
Запустите приложение и перейдите на страницу About. Количество зачисленных
студентов по дням отображается в таблице.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Добавление ссылок для сортировки столбцов
" Добавление поля поиска
" Добавление разбиения по страницам в указатель учащихся
" Добавление разбиения по страницам в метод Index
" Добавление ссылок для разбиения по страницам
" Создание страницы сведений
В следующем учебнике описано, как с помощью миграций обрабатывать
изменения в модели данных.
Далее: Обработка изменений модели данных
Учебник. Часть 5. Применение
миграций к примеру приложения
университета Contoso
Статья • 28.01.2023 • Чтение занимает 5 мин
В этом руководстве вы начнете использовать функцию EF Core миграции для
управления изменениями модели данных. В последующих руководствах вы
добавите дополнительные миграции по мере изменения модели данных.
Изучив это руководство, вы:
" Дополнительные сведения о миграциях
" Создание первоначальной миграции
" Обзор методов Up и Down
" Дополнительные сведения о моментальном снимке модели данных
" Применение миграции
Предварительные требования
Сортировка, фильтрация и разбиение на страницы
Сведения о миграциях
При разработке нового приложения ваша модель данных часто меняется, и при
каждом таком изменении она нарушает синхронизацию с базой данных. Вы начали
работу с этими руководствами, настроив Entity Framework для создания базы
данных, если она еще не существует. Затем при каждом изменении модели
данных — добавлении, удалении или изменении классов сущностей или изменении
класса DbContext — вы можете удалить базу данных, после чего EF создает новую,
которая соответствует данной модели, и заполняет ее тестовыми данными.
Этот способ для обеспечения синхронизации базы данных с моделью данных
хорошо работает до развертывания приложения в рабочей среде. Когда
приложение выполняется в рабочей среде, оно обычно хранит данные, которые
вы хотите сохранить, и вам нежелательно терять все при каждом изменении,
например при добавлении нового столбца. Функция EF Core миграции решает эту
проблему, позволяя EF обновлять схему базы данных вместо создания новой базы
данных.
Для миграции можно использовать консоль диспетчера пакетов (PMC) или CLI. Эти
руководства демонстрируют использование команд интерфейса командной строки.
Дополнительные сведения о PMC приведены в конце этого руководства.
Удалите базу данных:
Установите EF Core средства как глобальное средство и удалите базу данных:
Интерфейс командной строки.NET
dotnet tool install --global dotnet-ef
dotnet ef database drop
Следующий раздел описывает выполнение команд интерфейса командной строки.
Создание первоначальной миграции
Сохраните изменения и выполните сборку проекта. После этого откройте
командное окно и в папку проекта. Вот как это можно сделать быстро:
Щелкните правой кнопкой мыши проект в обозревателе решений и
выберите в контекстном меню пункт Открыть папку в проводнике.
Введите "cmd" в адресной строке и нажмите клавишу ВВОД.
Введите в командном окне следующую команду:
Интерфейс командной строки.NET
dotnet ef migrations add InitialCreate
Приведенные выше команды выводят результат наподобие следующего:
Консоль
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
Done. To undo this action, use 'ef migrations remove'
Если отображается сообщение об ошибке "Доступ к файлу... ContosoUniversity.dll
невозможен, так как файл используется другим процессом", найдите значок IIS
Express в области уведомлений Windows, щелкните его правой кнопкой мыши, а
затем выберите ContosoUniversity > Остановить сайт.
Обзор методов Up и Down
При выполнении команды migrations add система EF сформировала код, который
создаст базу данных с нуля. Этот код находится в файле
<timestamp>_InitialCreate.cs внутри папки Migrations. Метод Up класса
InitialCreate создает таблицы базы данных, которые соответствуют наборам
сущностей модели данных, а метод Down удаляет их, как показано в следующем
примере.
C#
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Course",
columns: table => new
{
CourseID = table.Column<int>(nullable: false),
Credits = table.Column<int>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Course", x => x.CourseID);
});
// Additional code not shown
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Enrollment");
// Additional code not shown
}
}
Функция миграций вызывает метод Up , чтобы реализовать изменения модели
данных для миграции. При вводе команды для отката обновления функция
миграций вызывает метод Down .
Этот пример кода предназначен для первоначальной миграции, созданной при
вводе команды migrations add InitialCreate . Параметр имени миграции (в
примере это "InitialCreate") используется в качестве имени файла и может быть
любым. Рекомендуется выбрать слово или фразу, которые кратко описывают
назначение миграции. Например, последнюю миграцию можно назвать
"AddDepartmentTable".
Если вы создали первоначальную миграцию, когда база данных уже существовала,
код для создания базы данных формируется, но выполнять его не требуется, так как
база данных уже соответствует модели данных. Однако при развертывании
приложения в другой среде, где база данных еще не существует, этот код будет
выполняться для создания базы данных, поэтому рекомендуется сначала его
протестировать. Вот почему ранее вы удалили базу данных — это позволяет
функции миграций создать базу данных с нуля.
Моментальный снимок модели данных
Функция миграций создает моментальный снимок текущей схемы базы данных в
Migrations/SchoolContextModelSnapshot.cs . При добавлении миграции EF
определяет, что именно изменилось, сравнивая модель данных с файлом
моментального снимка.
Для удаления миграции используйте команду dotnet ef migrations remove. dotnet ef
migrations remove удаляет миграцию и гарантирует корректный сброс
моментального снимка. В случае сбоя dotnet ef migrations remove используйте
dotnet ef migrations remove -v , чтобы получить сведения об ошибке.
Дополнительные сведения об использовании файла моментального снимка смEF
Core. в разделе "Миграции в team Environments".
Применение миграции
Введите следующую команду в командном окне, чтобы создать базу данных и
таблицы в ней.
Интерфейс командной строки.NET
dotnet ef database update
Выходные данные команды аналогичны команде migrations add , за исключением
того, что вы видите журналы для команд SQL, настраивающих базу данных. В
приведенном ниже примере выходных данных большинство журналов опущено.
Если вам не нужен такой уровень детализации сообщений журнала, можно
изменить уровень ведения журнала в файле appsettings.Development.json .
Дополнительные сведения см. в разделе Ведение журнала в .NET Core и ASP.NET
Core.
text
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core initialized 'SchoolContext' using provider
'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (274ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
CREATE DATABASE [ContosoUniversity2];
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (60ms) [Parameters=[], CommandType='Text',
CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [ContosoUniversity2] SET READ_COMMITTED_SNAPSHOT
ON;
END;
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (15ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
<logs omitted for brevity>
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text',
CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20190327172701_InitialCreate', N'5.0-rtm');
Done.
Используйте обозреватель объектов SQL Server для проверки базы данных, как
описано в первом руководстве. Вы заметите добавление таблицы
__EFMigrationsHistory, отслеживающей миграции, которые были применены к базе
данных. Просмотрев данные в этой таблице, вы увидите одну строку для первой
миграции. (Последний журнал в предыдущем примере выходных данных
интерфейса командной строки показывает оператор INSERT, создающий эту
строку.)
Запустите приложение, чтобы убедиться, что все работает, как и раньше.
Сравнение CLI и PMC
Средства EF для управления миграциями доступны в виде команд интерфейса
командной строки .NET Core или командлетов PowerShell в окне консоли
диспетчера пакетов Visual Studio. Это руководство описывает использование
интерфейса командной строки, но вы можете использовать и консоль диспетчера
пакетов.
Команды EF для команд консоли диспетчера пакетов находятся в пакете
Microsoft.EntityFrameworkCore.Tools . Этот пакет входит в метапакет
Microsoft.AspNetCore.App, поэтому если приложение уже содержит ссылку на пакет
Microsoft.AspNetCore.App , добавлять ссылку на пакет не нужно.
Внимание! Это не тот же пакет, который вы устанавливаете для CLI, изменив файл
.csproj . Его имя заканчивается на Tools , а имя пакета интерфейса командной
строки — на Tools.DotNet .
Дополнительные сведения о командах интерфейса командной строки см. в разделе
.NET Core CLI.
Дополнительные сведения о командах консоли диспетчера пакетов см. в разделе
Консоль диспетчера пакетов (Visual Studio).
Получение кода
Скачайте или ознакомьтесь с готовым приложением.
Следующий шаг
В следующем учебнике описаны более сложные вопросы, связанные с
развертыванием модели данных. Попутно вы создадите и примените
дополнительные миграции.
Создание и применение дополнительных миграций
Руководство. Создание сложной
модели данных — ASP.NET MVC с
помощью EF Core
Статья • 28.01.2023 • Чтение занимает 27 мин
В предыдущих руководствах вы работали с простой моделью данных, состоящей из
трех сущностей. В этом руководстве вы добавите дополнительные сущности и
связи, а также настроите модель данных, указав правила форматирования,
проверки и сопоставления базы данных.
По завершении работы классы сущностей сформируют готовую модель данных,
приведенную на следующем рисунке:
В этом учебнике рассмотрены следующие задачи.
" Настройка модели данных
" Изменения сущности Student
" Создание сущности Instructor
" Создание сущности OfficeAssignment
" Изменение сущности Course
" Создание сущности Department
" Изменение сущности Enrollment
" Обновление контекста базы данных
" Начальное заполнение базы данных тестовыми данными
" Добавление миграции
" Изменение строки подключения
" Обновление базы данных
Предварительные требования
Использование EF Core миграций
Настройка модели данных
В этом разделе вы узнаете, как настроить модель данных с помощью атрибутов,
которые указывают правила форматирования, проверки и сопоставления базы
данных. Затем в нескольких последующих разделах вы создаете всю модель
данных School целиком, добавив атрибуты к уже созданным классам и создав
классы для остальных типов сущностей в модели.
Атрибут DataType
Сейчас для дат зачисления студентов учащихся все веб-страницы отображают
время и дату, хотя для этого поля достаточно одной даты. Используя атрибуты
заметок к данным, вы можете внести в код одно изменение, позволяющее
исправить формат отображения в каждом представлении, где отображаются эти
данные. Чтобы рассмотреть соответствующий пример, вы добавите атрибут в
свойство EnrollmentDate класса Student .
В Models/Student.cs добавьте оператор using для пространства имен
System.ComponentModel.DataAnnotations , а также атрибуты DataType и DisplayFormat
для свойства EnrollmentDate , как показано в следующем примере:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Атрибут DataType позволяет указать тип данных с более точным определением по
сравнению со встроенным типом базы данных. В этом случае требуется
отслеживать только дату, а не дату и время. В перечислении DataType представлено
множество типов данных, таких как Date, Time, PhoneNumber, Currency,
EmailAddress и других. Атрибут DataType также обеспечивает автоматическое
предоставление функций для определенных типов в приложении. Например,
может быть создана ссылка mailto: для DataType.EmailAddress . Также в браузерах с
поддержкой HTML5 может быть предоставлен селектор даты для DataType.Date .
Атрибут DataType создает атрибуты HTML 5 data- , которые используются
браузерами с поддержкой HTML 5. Атрибуты DataType не предназначены для
проверки.
DataType.Date не задает формат отображаемой даты. По умолчанию поле данных
отображается с использованием форматов, установленных в CultureInfo сервера.
С помощью атрибута DisplayFormat можно явно указать формат даты:
C#
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode =
true)]
Параметр ApplyFormatInEditMode указывает, что формат также должен применяться
при отображении значения в текстовом поле для редактирования. (В некоторых
случаях такое поведение нежелательно. Например, в текстовом поле для
редактирования денежных значений обычно не требуется отображать символ
валюты.)
Атрибут DisplayFormat можно использовать отдельно, однако чаще всего его
рекомендуется применять вместе с атрибутом DataType . Атрибут DataType передает
семантику данных (в отличие от способа их вывода на экран) и дает следующие
преимущества по сравнению с DisplayFormat :
Поддержка функций HTML5 в браузере (отображение элемента управления
календарем, соответствующего языковому стандарту символа валюты, ссылок
электронной почты, проверки на стороне клиента и т. д.).
По умолчанию формат отображения данных в браузере определяется в
соответствии с установленным языковым стандартом.
Дополнительные сведения см. в документации по вспомогательной функции тегов
<input>.
Запустите приложение, перейдите на страницу индекса студентов учащихся и
обратите внимание, что время для дат зачисления больше не отображается.
Аналогичная ситуация будет наблюдаться в любом представлении, использующем
модель Student.
Атрибут StringLength
С помощью атрибутов также можно указать правила проверки данных и
сообщения об ошибках проверки. Атрибут StringLength задает максимальную
длину в базе данных и обеспечивает проверку на стороне клиента и на стороне
сервера для ASP.NET Core MVC. В этом атрибуте также можно указать
минимальную длину строки, но это минимальное значение не влияет на схему
базы данных.
Предположим, вы хотите сделать так, чтобы пользователи не вводили больше 50
символов для имени. Чтобы задать это ограничение, добавьте атрибуты
StringLength в свойства LastName и FirstMidName , как показано в следующем
примере:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Атрибут StringLength не запрещает пользователю ввести пробел в качестве имени
пользователя. Атрибут RegularExpression можно использовать для применения
ограничений к входным данным. Например, следующий код требует, чтобы
первый символ был прописной буквой, а остальные символы были буквенными:
C#
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
Атрибут MaxLength предоставляет функциональность, аналогичную атрибуту
StringLength , но не обеспечивает проверку на стороне клиента.
Модель базы данных изменилась до такой степени, что требуется изменение схемы
базы данных. Миграции позволяют обновить схему без потери данных, которые вы
могли добавить в базу данных с помощью пользовательского интерфейса
приложения.
Сохраните изменения и выполните сборку проекта. Затем откройте командное
окно в папке проекта и введите следующие команды:
Интерфейс командной строки.NET
dotnet ef migrations add MaxLengthOnNames
Интерфейс командной строки.NET
dotnet ef database update
Команда migrations add выдает предупреждение о возможной потере данных, так
как это изменение сокращает максимальную длину для двух столбцов. Функция
миграций создает файл с именем <timeStamp>_MaxLengthOnNames.cs . Он содержит в
методе Up код, который обновит базу данных в соответствии с текущей моделью
данных. Команда database update запустила этот код.
Метка времени, добавленная в качестве префикса к имени файла миграций,
используется платформой Entity Framework для упорядочения миграций. Вы
можете создать несколько миграций перед выполнением команды updatedatabase, после чего все миграции применяются в порядке их создания.
Запустите приложение, выберите вкладку Students (Учащиеся), щелкните Create
New (Создать) и попробуйте ввести любое имя длиннее 50 символов. Приложение
должно отобразить ошибку.
Атрибут Column
Вы также можете использовать атрибуты, чтобы управлять сопоставлением классов
и свойств с базой данных. Предположим, что вы использовали имя FirstMidName
для поля имени, так как это поле также может содержать отчество. Но вам нужно,
чтобы столбец базы данных назывался FirstName , так как к этому имени привыкли
пользователи, которые будут составлять нерегламентированные запросы к базе
данных. Чтобы выполнить это сопоставление, можно использовать атрибут Column .
Атрибут Column указывает, что при создании базы данных столбец таблицы
Student , сопоставляемый со свойством FirstMidName , будет называться FirstName .
Другими словами, когда ваш код ссылается на Student.FirstMidName , данные будут
браться из столбца FirstName таблицы Student или обновляться в нем. Если не
указать имена столбцов, им назначается имя, совпадающее с именем свойства.
В файле Student.cs добавьте оператор using для
System.ComponentModel.DataAnnotations.Schema и атрибут имени столбца в свойство
FirstMidName , как показано в следующем выделенном коде:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Добавление атрибута Column изменяет модель для поддержки SchoolContext ,
поэтому она не будет соответствовать базе данных.
Сохраните изменения и выполните сборку проекта. Затем откройте командное
окно в папке проекта и введите следующие команды, чтобы создать другую
миграцию:
Интерфейс командной строки.NET
dotnet ef migrations add ColumnFirstName
Интерфейс командной строки.NET
dotnet ef database update
В обозревателе объектов SQL Server откройте конструктор таблиц учащихся,
дважды щелкнув таблицу Student (Учащийся).
До применения двух первых миграций столбцы имен имели тип nvarchar(MAX).
Теперь они относятся к типу nvarchar(50), а имя столбца изменилось с FirstMidName
на FirstName.
7 Примечание
Если попытаться выполнить компиляцию до создания всех классов сущностей
в следующих разделах, могут возникнуть ошибки компилятора.
Изменения сущности Student
В Models/Student.cs замените добавленный ранее код на приведенный ниже.
Изменения выделены.
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50)]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Атрибут Required
Атрибут Required делает свойства имен обязательными полями. Атрибут Required
не нужен для типов, не допускающих значения null, например для типов значений
(DateTime, int, double, float и т. д.). Типы, которые не могут принимать значение null,
автоматически обрабатываются как обязательные поля.
Для применения MinimumLength нужно использовать атрибут Required с
MinimumLength .
C#
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
Атрибут Display
Атрибут Display указывает, что заголовки для текстовых полей должны иметь вид
"First Name" (Имя), "Last Name" (Фамилия), "Full Name" (Полное имя) и "Enrollment
Date" (Дата зачисления) вместо имени свойства в каждом экземпляре (в котором
не используется пробел для разделения слов).
Вычисляемое свойство FullName
FullName — это вычисляемое свойство, которое возвращает значение, созданное
путем объединения двух других свойств. Поэтому оно имеет только метод доступа
get, и в базе данных не будет создан столбец FullName .
Создание сущности Instructor
Создайте файл Models/Instructor.cs , заменив код шаблона следующим:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<CourseAssignment> CourseAssignments { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Обратите внимание, что некоторые свойства являются одинаковыми в сущностях
Student и Instructor. В руководстве по реализации наследования далее в этой серии
вы выполните рефакторинг данного кода, чтобы устранить избыточность.
Несколько атрибутов можно расположить на одной строке, поэтому записать
атрибуты HireDate можно следующим образом:
C#
[DataType(DataType.Date),Display(Name = "Hire
Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
Свойства навигации CourseAssignments и
OfficeAssignment
CourseAssignments и OfficeAssignment — это свойства навигации.
Преподаватель может проводить любое количество курсов, поэтому
CourseAssignments определен как коллекция.
C#
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Если свойство навигации может содержать несколько сущностей, оно должно
иметь тип списка, допускающий добавление, удаление и обновление записей. Вы
можете указать тип ICollection<T> либо, например, тип List<T> или HashSet<T> .
Если указан тип ICollection<T> , платформа EF по умолчанию создает коллекцию
HashSet<T> .
Причина того, что это сущности CourseAssignment , описана ниже в разделе о связях
многие ко многим.
Бизнес-правила университета Contoso указывают, что преподаватель может иметь
не более одного кабинета, поэтому свойство OfficeAssignment содержит отдельную
сущность OfficeAssignment (которая может иметь значение null, если кабинет не
назначен).
C#
public OfficeAssignment OfficeAssignment { get; set; }
Создание сущности OfficeAssignment
Создайте Models/OfficeAssignment.cs , используя следующий код:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
Атрибут Key
Между сущностями Instructor и OfficeAssignment действует связь "один к нулю
или к одному". Назначение кабинета существует только в связи с преподавателем,
которому оно назначено, поэтому его первичный ключ также является внешним
ключом для сущности Instructor . Однако Entity Framework не распознает
InstructorID в качестве первичного ключа этой сущности автоматически, так как
ее имя не соответствует соглашению об именовании ID или classnameID . Таким
образом, атрибут Key используется для определения ее в качестве ключа:
C#
[Key]
public int InstructorID { get; set; }
Атрибут Key также можно использовать, если сущность имеет собственный
первичный ключ, но нужно задать для свойства имя, отличное от classnameID или
ID.
По умолчанию EF считает ключ созданным не базой данных, так как столбец
предназначен для идентифицирующего отношения.
Свойство навигации Instructor
Сущность Instructor имеет свойство навигации OfficeAssignment , допускающее
значение null (так как у преподавателя может не быть назначения кабинета), а
сущность OfficeAssignment имеет свойство навигации Instructor , не допускающее
значение null (так как назначение кабинета не может существовать без
преподавателя — InstructorID не допускает значение null). Когда сущность
Instructor имеет связанную сущность OfficeAssignment, каждая из них имеет ссылку
на другую в своем свойстве навигации.
Можно поместить атрибут [Required] в свойство навигации Instructor, чтобы
указать, что должен присутствовать связанный преподаватель, однако это
необязательно, так как внешний ключ InstructorID (который также является
ключом для этой таблицы) не допускает значение null.
Изменение сущности Course
В Models/Course.cs замените добавленный ранее код на приведенный ниже.
Изменения выделены.
C#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
Сущность курса имеет свойство внешнего ключа DepartmentID , указывающее на
связанную сущность Department, а также она имеет свойство навигации
Department .
Платформа Entity Framework не требует добавлять свойство внешнего ключа в
модель данных при наличии свойства навигации для связанной сущности. EF
автоматически создает внешние ключи в базе данных по мере необходимости, а
также создает для них теневые свойства. Однако наличие внешнего ключа в
модели данных позволяет сделать обновления проще и эффективнее. Например,
при извлечении сущности Course для редактирования сущность Department имеет
значение NULL, если вы ее не загружаете. Таким образом, при обновлении
сущности Course вам потребуется сначала получить сущность Department . Если
свойство внешнего ключа DepartmentID включено в модель данных, получать
сущность Department перед обновлением не нужно.
Атрибут DatabaseGenerated
Атрибут DatabaseGenerated с параметром None в свойстве CourseID указывает, что
значения первичного ключа предоставлены пользователем, а не созданы базой
данных.
C#
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
По умолчанию Entity Framework предполагает, что значения первичного ключа
созданы базой данных. Именно это и требуется для большинства сценариев.
Однако для сущностей Course вы будете использовать определяемый
пользователем номер курса, например серия 1000 для одной кафедры, серия 2000
для другой и так далее.
Атрибут DatabaseGenerated также можно использовать для создания значения по
умолчанию, как в случае, когда столбцы базы данных используются для записи
даты, когда строка была создана или обновлена. Дополнительные сведения см. в
разделе Созданные свойства.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и свойства навигации в сущности Course отражают
следующие связи:
Курс назначается одной кафедре, поэтому по указанным выше причинам имеется
внешний ключ DepartmentID и свойство навигации Department .
C#
public int DepartmentID { get; set; }
public Department Department { get; set; }
На курс может быть зачислено любое количество учащихся, поэтому свойство
навигации Enrollments является коллекцией:
C#
public ICollection<Enrollment> Enrollments { get; set; }
Курс могут вести несколько преподавателей, поэтому свойство навигации
CourseAssignments является коллекцией (тип CourseAssignment описан ниже):
C#
public ICollection<CourseAssignment> CourseAssignments { get; set; }
Создание сущности Department
Создайте Models/Department.cs , используя следующий код:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Атрибут Column
Ранее атрибут Column использовался, чтобы изменить сопоставление имени
столбца. В коде для сущности Department атрибут Column используется для
изменения сопоставления типов данных SQL, поэтому столбец будет определяться
с помощью типа money SQL Server в базе данных:
C#
[Column(TypeName="money")]
public decimal Budget { get; set; }
Сопоставление столбцов обычно не требуется, так как Entity Framework выбирает
подходящий тип данных SQL Server на основе типа среды CLR, определяемого вами
для свойства. Тип decimal среды CLR сопоставляется с типом decimal SQL Server. Но
в этом случае вы знаете, что столбец будет содержать суммы в валюте, хотя для
этого лучше подходит тип данных money.
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Кафедра может иметь или не иметь администратора, и администратор всегда
является преподавателем. Поэтому InstructorID свойство включается в качестве
внешнего ключа в сущность Instructor, а после int обозначения типа добавляется
знак вопроса, указывающий, что свойство допускает значение null. Свойство
навигации называется Administrator , но содержит сущность Instructor:
C#
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
Кафедра может иметь несколько курсов, поэтому доступно свойство навигации
Courses:
C#
public ICollection<Course> Courses { get; set; }
7 Примечание
По соглашению Entity Framework разрешает каскадное удаление для внешних
ключей, не допускающих значение null, и связей многие ко многим. Это может
привести к циклическим правилам каскадного удаления, которые вызывают
исключение при попытке добавить миграцию. Например, если вы не
определили свойство Department.InstructorID как допускающее значение
NULL, EF настраивает правило каскадного удаления для удаления кафедры при
удалении преподавателя, что вам не нужно. Если бизнес-правила требуют,
чтобы свойство InstructorID не допускало значение null, используйте
следующий оператор текучего API, чтобы отключить каскадное удаление для
этой связи:
C#
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
Изменение сущности Enrollment
В Models/Enrollment.cs замените добавленный ранее код на приведенный ниже:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
Свойства внешнего ключа и навигации
Свойства внешнего ключа и навигации отражают следующие связи:
Запись зачисления предназначена для одного курса, поэтому доступно свойство
первичного ключа CourseID и свойство навигации Course :
C#
public int CourseID { get; set; }
public Course Course { get; set; }
Запись зачисления предназначена для одного учащегося, поэтому доступно
свойство первичного ключа StudentID и свойство навигации Student :
C#
public int StudentID { get; set; }
public Student Student { get; set; }
Связи "многие ко многим"
Между сущностями Student и Course имеется связь "многие ко многим", а
сущность Enrollment выступает в качестве таблицы соединения "многие ко многим"
с полезными данными в базе данных. Фраза "с полезными данными" означает, что
таблица Enrollment содержит дополнительные данные, кроме внешних ключей для
присоединяемых таблиц (в данном случае — первичный ключ и свойство Grade ).
На следующем рисунке показано, как выглядят эти связи на схеме сущностей. (Эта
схема была создана с помощью Entity Framework Power Tools для EF 6.x. Создание
схемы не является частью руководства, оно просто используется здесь в качестве
примера.)
Каждая линия связи имеет 1 на одном конце и звездочку (*) на другом, указывая
характер один ко многим.
Если таблица Enrollment не включала в себя сведения об оценках, ей потребуется
содержать всего два внешних ключа — CourseID и StudentID . В данном случае это
будет таблица соединения многие ко многим без полезных данных (ее также
называют чистой таблицей соединения) в базе данных. Между сущностями
Instructor и Course действует связь "многие ко многим", и следующим шагом
является создание класса сущности, выступающего в качестве таблицы соединения
без полезных данных.
EF Core поддерживает неявные таблицы соединения для связей "многие ко
многим", но этот обучающий класс не был обновлен для использования таблицы
неявного соединения. См. раздел Связи "многие ко многим" в обновленной версии
этого учебника по Razor Pages.
Сущность CourseAssignment
Создайте Models/CourseAssignment.cs , используя следующий код:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}
Имена для сущностей соединения
В базе данных для связи многие ко многим между Instructor и Courses нужна
таблица соединения, которая должна быть представлена набором сущностей.
Обычно для сущности соединения используется имя EntityName1EntityName2 ,
которое в данном случае будет иметь значение CourseInstructor . Однако
рекомендуется выбрать имя, которое описывает эту связь. Модели данных
создаются простыми и разрастаются, а соединения без полезных данных часто
начинают включать эти данные позднее. Если вначале задать описательное имя
сущности, его не потребуется менять позднее. Оптимально, если сущность
соединения имеет собственное естественное имя (возможно, из одного слова) в
бизнес-среде. Например, Books и Customers можно связать через Ratings. Для этой
связи CourseAssignment подходит лучше, чем CourseInstructor .
Составной ключ
Так как внешние ключи не допускают значение null и совместно однозначно
определяют каждую строку таблицы, отдельный первичный ключ не требуется.
Свойства InstructorID и CourseID должны выступать в качестве составного
первичного ключа. Единственным способом указать составные первичные ключи
для EF является текучий API (с помощью атрибутов это сделать невозможно).
Настройка составного первичного ключа описана в следующем разделе.
Составной ключ позволяет использовать несколько строк для одного курса и
несколько строк для одного преподавателя, а также не позволяет использовать
несколько строк для одного преподавателя и курса. Сущность соединения
Enrollment определяет собственный первичный ключ, поэтому подобные
дубликаты возможны. Для предотвращения таких повторяющихся значений
добавьте уникальный индекс для полей внешнего ключа или настройте Enrollment
с первичным составным ключом аналогично CourseAssignment . Дополнительные
сведения см. в разделе Индексы.
Обновление контекста базы данных
Добавьте следующий выделенный код в файл Data/SchoolContext.cs :
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
public
public
public
public
public
public
public
DbSet<Course> Courses { get; set; }
DbSet<Enrollment> Enrollments { get; set;
DbSet<Student> Students { get; set; }
DbSet<Department> Departments { get; set;
DbSet<Instructor> Instructors { get; set;
DbSet<OfficeAssignment> OfficeAssignments
DbSet<CourseAssignment> CourseAssignments
}
}
}
{ get; set; }
{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>
().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>
().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
Этот код добавляет новые сущности и настраивает составной первичный ключ
сущности CourseAssignment.
Сведения об альтернативе текучему API
Код в предыдущем методе OnModelCreating класса DbContext использует для
настройки поведения EF текучий API. API называется "fluent", так как он часто
используется путем объединения ряда вызовов методов в одну инструкцию, как в
этом примере из EF Core документации:
C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
В этом руководстве текучий API используется только для сопоставления базы
данных, которое невозможно выполнить с помощью атрибутов. Однако текучий
API позволяет задать большинство правил форматирования, проверки и
сопоставления, которые можно указать с помощью атрибутов. Некоторые
атрибуты, такие как MinimumLength , невозможно применить с текучим API. Как
упоминалось ранее, MinimumLength не изменяет схему, а лишь применяет правило
проверки на стороне клиента и сервера.
Некоторые разработчики предпочитают использовать текучий API монопольно,
чтобы оставить свои классы сущностей "чистыми". Атрибуты и текучий API можно
смешивать, и существует несколько конфигураций, которые можно реализовать
только с помощью текучего API. На практике рекомендуется выбрать один из этих
двух подходов и использовать его максимально согласованно. Если вы используете
оба, обратите внимание, что при любом конфликте текучий API переопределяет
атрибуты.
Дополнительные сведения о сравнении атрибутов и текучего API см. в разделе
Методы конфигурации.
Схема сущностей, показывающая связи
Ниже показана схема, создаваемая средствами Entity Framework Power Tools для
завершенной модели School.
Кроме линий связей "один ко многим" (1 к *), здесь можно видеть линию связи
"один к нулю или к одному" (1 к 0...1) между сущностями Instructor и
OfficeAssignment , а также линию связи "нуль или один ко многим" (0...1 к *) между
сущностями Instructor и Department.
Начальное заполнение базы данных
тестовыми данными
Замените код в файле Data/DbInitializer.cs на приведенный ниже, чтобы
предоставить начальные данные для созданных вами сущностей.
C#
using
using
using
using
using
System;
System.Linq;
Microsoft.EntityFrameworkCore;
Microsoft.Extensions.DependencyInjection;
ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return;
// DB has been seeded
}
var students = new Student[]
{
new Student { FirstMidName = "Carson",
LastName =
"Alexander",
EnrollmentDate = DateTime.Parse("2010-09-01") },
new Student { FirstMidName = "Meredith", LastName =
"Alonso",
EnrollmentDate = DateTime.Parse("2012-09-01")
new Student { FirstMidName = "Arturo",
LastName
EnrollmentDate = DateTime.Parse("2013-09-01")
new Student { FirstMidName = "Gytis",
LastName
},
= "Anand",
},
=
EnrollmentDate = DateTime.Parse("2012-09-01")
new Student { FirstMidName = "Yan",
LastName
EnrollmentDate = DateTime.Parse("2012-09-01")
new Student { FirstMidName = "Peggy",
LastName
},
= "Li",
},
=
"Barzdukas",
"Justice",
EnrollmentDate = DateTime.Parse("2011-09-01") },
new Student { FirstMidName = "Laura",
LastName =
"Norman",
EnrollmentDate = DateTime.Parse("2013-09-01") },
new Student { FirstMidName = "Nino",
LastName =
"Olivetto",
EnrollmentDate = DateTime.Parse("2005-09-01") }
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var instructors = new Instructor[]
{
new Instructor { FirstMidName = "Kim",
LastName
"Abercrombie",
HireDate = DateTime.Parse("1995-03-11") },
new Instructor { FirstMidName = "Fadi",
LastName
"Fakhouri",
HireDate = DateTime.Parse("2002-07-06") },
new Instructor { FirstMidName = "Roger",
LastName
"Harui",
HireDate = DateTime.Parse("1998-07-01") },
new Instructor { FirstMidName = "Candace", LastName
"Kapoor",
HireDate = DateTime.Parse("2001-01-15") },
new Instructor { FirstMidName = "Roger",
LastName
"Zheng",
HireDate = DateTime.Parse("2004-02-12") }
};
=
=
=
=
=
foreach (Instructor i in instructors)
{
context.Instructors.Add(i);
}
context.SaveChanges();
var departments = new Department[]
{
new Department { Name = "English",
Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Abercrombie").ID },
new Department { Name = "Mathematics", Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Fakhouri").ID },
new Department { Name = "Engineering", Budget = 350000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Harui").ID },
new Department { Name = "Economics",
Budget = 100000,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName ==
"Kapoor").ID }
};
foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();
var courses = new Course[]
{
new Course {CourseID = 1050, Title = "Chemistry",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition",
Credits = 3,
DepartmentID = departments.Single( s => s.Name ==
"English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature",
Credits = 4,
DepartmentID = departments.Single( s => s.Name ==
"English").DepartmentID
},
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var officeAssignments = new OfficeAssignment[]
{
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Fakhouri").ID,
Location = "Smith 17" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Harui").ID,
Location = "Gowan 27" },
new OfficeAssignment {
InstructorID = instructors.Single( i => i.LastName ==
"Kapoor").ID,
Location = "Thompson 304" },
};
foreach (OfficeAssignment o in officeAssignments)
{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();
var courseInstructors = new CourseAssignment[]
{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title
"Microeconomics" ).CourseID,
InstructorID = instructors.Single(i =>
"Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title
"Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i =>
"Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title
).CourseID,
InstructorID = instructors.Single(i =>
"Fakhouri").ID
},
==
i.LastName ==
==
i.LastName ==
== "Calculus"
i.LastName ==
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature"
).CourseID,
InstructorID = instructors.Single(i => i.LastName ==
"Abercrombie").ID
},
};
foreach (CourseAssignment ci in courseInstructors)
{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID,
Grade = Grade.A
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title ==
"Microeconomics" ).CourseID,
Grade = Grade.C
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alexander").ID,
CourseID = courses.Single(c => c.Title ==
"Macroeconomics" ).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Calculus"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Trigonometry"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Alonso").ID,
CourseID = courses.Single(c => c.Title == "Composition"
).CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Anand").ID,
CourseID = courses.Single(c => c.Title == "Chemistry"
).CourseID
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Anand").ID,
CourseID = courses.Single(c => c.Title ==
"Microeconomics").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Barzdukas").ID,
CourseID = courses.Single(c => c.Title ==
"Chemistry").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName == "Li").ID,
CourseID = courses.Single(c => c.Title ==
"Composition").CourseID,
Grade = Grade.B
},
new Enrollment {
StudentID = students.Single(s => s.LastName ==
"Justice").ID,
CourseID = courses.Single(c => c.Title ==
"Literature").CourseID,
Grade = Grade.B
}
};
foreach (Enrollment e in enrollments)
{
var enrollmentInDataBase = context.Enrollments.Where(
s =>
s.Student.ID == e.StudentID &&
s.Course.CourseID ==
e.CourseID).SingleOrDefault();
if (enrollmentInDataBase == null)
{
context.Enrollments.Add(e);
}
}
context.SaveChanges();
}
}
}
Как можно было заметить в первом руководстве, основная часть кода просто
создает объекты сущности и загружает демонстрационные данные в свойства для
тестирования. Обратите внимание, как обрабатываются связи многие ко многим:
код формирует связи, создавая сущности в наборах сущностей соединения
Enrollments и CourseAssignment .
Добавление миграции
Сохраните изменения и выполните сборку проекта. Затем откройте командное
окно в папке проекта и введите команду migrations add (команду update-database
пока не выполняйте):
Интерфейс командной строки.NET
dotnet ef migrations add ComplexDataModel
Вы получаете предупреждение о возможной потере данных.
text
An operation was scaffolded that may result in the loss of data. Please
review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
Если попытаться выполнить команду database update на этом этапе (пока этого
делать не нужно), возникнет следующая ошибка:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint
"FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database
"ContosoUniversity", table "dbo.Department", column 'DepartmentID'. (Оператор
ALTER TABLE конфликтовал с ограничением FOREIGN KEY
"FK_dbo.Course_dbo.Department_DepartmentID". Конфликт возник в столбце
"DepartmentID" таблицы "dbo.Department" базы данных "ContosoUniversity".)
Иногда при выполнении миграций с существующими данными необходимо
вставить данные-заглушки в базу данных для соблюдения ограничений внешнего
ключа. Созданный код в методе Up добавляет внешний ключ DepartmentID , не
допускающий значение NULL, в таблицу Course . Если при запуске кода в таблице
Course уже имеются строки, операция AddColumn завершается неудачей, так как SQL
Server не знает, какое значение поставить в столбце, который не допускает
значение null. В этом учебнике вы запустите миграцию в новую базу данных, но в
реальном приложении потребуется обеспечить обработку существующих данных в
этой миграции, соответствующий пример приведен ниже.
Чтобы заставить эту миграцию работать с существующими данными, нужно
изменить код, чтобы присвоить новому столбцу значение по умолчанию, а также
создать кафедру-заглушку с именем "Temp" для использования по умолчанию. В
результате существующие строки Course будут связаны с кафедрой "Temp" после
выполнения метода Up .
Откройте файл {timestamp}_ComplexDataModel.cs .
Закомментируйте строку кода, которая добавляет столбец DepartmentID в
таблицу Course.
C#
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Course",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldNullable: true);
//migrationBuilder.AddColumn<int>(
//
name: "DepartmentID",
//
table: "Course",
//
nullable: false,
//
defaultValue: 0);
Добавьте выделенный ниже код после кода, создающего таблицу Department:
C#
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(nullable: true),
Name = table.Column<string>(maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Department", x => x.DepartmentID);
table.ForeignKey(
name: "FK_Department_Instructor_InstructorID",
column: x => x.InstructorID,
principalTable: "Instructor",
principalColumn: "ID",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget,
StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
nullable: false,
defaultValue: 1);
В реальном приложении вам потребуется написать код или сценарии для
добавления строк Department, а также для связи строк Course с новыми строками
Department. После этого кафедра "Temp" и значение по умолчанию в столбце
Course.DepartmentID вам больше не понадобятся.
Сохраните изменения и выполните сборку проекта.
Изменение строки подключения
Теперь у вас есть новый код в классе DbInitializer , который добавляет начальные
данные для новых сущностей в пустую базу данных. Чтобы указать EF создать
пустую базу данных, в файле appsettings.json измените имя базы данных в строке
подключения на ContosoUniversity3 или другое имя, которое вы еще не
использовали на компьютере, с которым работаете.
JSON
{
"ConnectionStrings": {
"DefaultConnection": "Server=
(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;
MultipleActiveResultSets=true"
},
Сохраните изменения в файл appsettings.json .
7 Примечание
Вместо изменения имени базы данных можно удалить ее. Воспользуйтесь
обозревателем объектов SQL Server (SSOX) или командой интерфейса
командной строки database drop :
Интерфейс командной строки.NET
dotnet ef database drop
Обновление базы данных
После изменения имени базы данных или ее удаления запустите команду database
update в командном окне, чтобы выполнить миграции.
Интерфейс командной строки.NET
dotnet ef database update
Запустите приложение, чтобы метод DbInitializer.Initialize запустился и
заполнил новую базу данных.
Откройте базу данных в SSOX, как уже делали это раньше, а затем разверните узел
Таблицы, чтобы увидеть все созданные таблицы. (Если SSOX уже был открыт,
нажмите кнопку Обновить.)
Запустите приложение, чтобы активировать код инициализатора, заполняющий
базу данных.
Щелкните правой кнопкой мыши таблицу CourseAssignment и выберите пункт
Просмотреть данные, чтобы убедиться в наличии данных.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Настройка модели данных
" Изменения сущности Student
" Создание сущности Instructor
" Создание сущности OfficeAssignment
" Изменение сущности Course
" Создание сущности Department
" Изменение сущности Enrollment
" Обновление контекста базы данных
" Начальное заполнение базы данных тестовыми данными
" Добавление миграции
" Изменение строки подключения
" Обновление базы данных
В следующем руководстве описано, как получить доступ к связанным данным.
Далее: Получение доступа к связанным данным
Руководство. Чтение связанных
данных — ASP.NET MVC с помощью EF
Core
Статья • 28.01.2023 • Чтение занимает 12 мин
В предыдущем руководстве мы завершили разработку модели данных School. Из
этого руководства вы узнаете, как читать и отображать связанные данные —
данные, которые Entity Framework загружает в свойства навигации.
На следующих рисунках изображены страницы, с которыми вы будете работать.
В этом учебнике рассмотрены следующие задачи.
" Загрузка связанных данных
" Создание страницы курсов
" Создание страницы преподавателей
" Дополнительные сведения о явной загрузке
Предварительные требования
Создание сложной модели данных
Загрузка связанных данных
Существует несколько способов, которыми программное обеспечение объектнореляционного сопоставления (ORM), такое как Entity Framework, может загружать
связанные данные в свойства навигации сущности:
Безотложная загрузка. При чтении сущности связанные данные извлекаются
вместе с ней. Обычно такая загрузка представляет собой одиночный запрос с
соединением, который получает все необходимые данные. Настроить Entity
Framework Core на использование безотложной загрузки можно при помощи
методов Include и ThenInclude .
Вы можете получить часть данных в отдельных запросах, и EF "исправит"
свойства навигации. То есть EF автоматически добавляет раздельно
извлеченные сущности к соответствующим свойствам навигации ранее
извлеченных объектов. Для запроса, получающего связанные данные, можно
использовать метод Load вместо метода, который возвращает список или
объект, такого как ToList или Single .
Явная загрузка. При первом чтении сущности связанные данные не
извлекаются. Если требуется получение связанных данных, то пишется
дополнительный код. Как и в случае безотложной загрузки с отдельными
запросами, явная загрузка представляет собой несколько запросов к базе
данных. Отличие заключается в том, что при явной загрузке в коде
указывается, какие свойства навигации будут загружены. В Entity Framework
Core 1.1 для выполнения явной загрузки можно использовать метод Load .
Пример:
Отложенная загрузка. При первом чтении сущности связанные данные не
извлекаются. Однако при первой попытке доступа к свойству навигации
необходимые для этого свойства навигации данные извлекаются
автоматически. Запрос к базе данных отправляется каждый раз, когда вы в
первый раз пытаетесь получить данные из свойства навигации. Entity
Framework Core 1.0 не поддерживает отложенную загрузку.
Особенности производительности
Если известно, что связанные данные потребуются для каждой полученной
сущности, то безотложная загрузка обычно обеспечивает наилучшую
производительность, поскольку одиночный запрос к базе данных обычно
эффективнее нескольких отдельных запросов для каждой полученной сущности.
Пусть, например, на каждом факультете есть десять связанных курсов. Безотложная
загрузка всех связанных данных приведет к одиночному запросу (с соединением) и
одним циклом приема-передачи данных из базы. Отдельные запросы по курсам
для каждого факультета приведут к одиннадцати циклам приема-передачи данных
из базы. При высокой задержке дополнительные циклы приема-передачи данных
особенно сильно влияют на производительность.
С другой стороны, в некоторых случаях отдельные запросы более эффективны.
Безотложная загрузка всех связанных данных в одном запросе может привести к
формированию очень сложного соединения, которое SQL сервер не сможет
эффективно обработать. Либо, если необходимо получить свойства навигации
сущности только для подмножества обрабатываемого набора сущностей,
отдельные запросы могут показать большую производительность, поскольку при
безотложной загрузке всех данных будет получено больше данных, чем вам
необходимо. Если важна производительность, то для выбора наилучшего решения
рекомендуется протестировать производительность для обоих случаев.
Создание страницы курсов
Сущность Course включает свойство навигации, которое содержит сущность
Department кафедры, к которому привязан курс. Чтобы отобразить в списке курсов
название связанной кафедры, необходимо получить свойство Name из сущности
Department , находящейся в свойстве навигации Course.Department .
Создайте для типа сущности Course контроллер с именем CoursesController с теми
же параметрами шаблона Контроллер MVC с представлениями, использующий
Entity Framework, которые мы ранее задали для контроллера StudentsController ,
как это показано на следующем рисунке:
Откройте CoursesController.cs и проверьте метод Index . В автоматически
сформированном шаблоне установлена безотложная загрузка свойства навигации
Department при помощи метода Include .
Замените метод Index следующим кодом, который использует более подходящее
имя для IQueryable , возвращающего сущности Course ( courses вместо
schoolContext ):
C#
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
Откройте Views/Courses/Index.cshtml и замените код шаблона следующим кодом:
Изменения выделены:
CSHTML
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
<a asp-action="Edit" asp-routeid="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-
id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-routeid="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Мы внесли следующие изменения в код шаблона:
Изменен заголовок с Index (Индекс) на Courses (Курсы).
Добавлен столбец Number (Номер), отображающий значение свойства
CourseID . По умолчанию в шаблоне отсутствуют первичные ключи, поскольку
для конечных пользователей они не имеют смысла. Однако в нашем случае
первичный ключ имеет смысл, и мы хотим его отобразить.
Изменен столбец Department (Кафедра) для отображения названия кафедры.
Код отображает свойство Name сущности Department , которая загружена в
свойство навигации Department :
HTML
@Html.DisplayFor(modelItem => item.Department.Name)
Для просмотра списка с названиями кафедр запустите приложение и выберите
вкладку Courses (Курсы).
Создание страницы преподавателей
В этом разделе мы создадим контроллер и представление для сущности Instructor ,
чтобы отобразить страницу Instructors:
Эта страница считывает и отображает связанные данные следующим образом:
Список преподавателей отображает связанные данные сущности
OfficeAssignment . Между сущностями Instructor и OfficeAssignment действует
связь один к нулю или к одному. Для сущностей OfficeAssignment установлена
безотложная загрузка. Как упоминалось ранее, безотложная загрузка обычно
эффективнее при получении связанных данных для всех строк главной
таблицы. В нашем случае мы хотим отобразить принадлежность к кабинету
для каждого преподавателя.
Когда пользователь выбирает преподавателя, отображаются связанные
сущности Course . Между сущностями Instructor и Course действует связь
многие ко многим. Для сущностей Course и связанных сущностей Department
используется безотложная загрузка. В этом случае отдельные запросы могут
оказаться эффективнее, поскольку нам требуются курсы только для
выбранного преподавателя. Этот пример, однако, показывает, как
использовать безотложную загрузку для свойств навигации сущностей,
которые сами находятся в свойствах навигации.
Когда пользователь выбирает курс, отображаются связанные данные из
набора сущностей Enrollments . Между сущностями Course и Enrollment
действует связь один ко многим. Для сущностей Enrollment и связанных с
ними сущностей Student используются отдельные запросы.
Создание модели для представления индекса
преподавателей
На странице "Instructors" (Преподаватели) отображаются данные из трех различных
таблиц. Таким образом, мы создаем модель представления, которая включает три
свойства, каждое из которых содержит данные из одной таблицы.
Создайте в папке SchoolViewModels файл InstructorIndexData.cs и замените
существующий код следующим кодом:
C#
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Создание контроллера и представлений Instructor
Создайте контроллер Instructors с действиями чтения/записи Entity Framework, как
показано на следующем рисунке:
Откройте файл InstructorsController.cs и добавьте директиву using для
пространства имен ViewModels:
C#
using ContosoUniversity.Models.SchoolViewModels;
Замените код Index следующим кодом для выполнения безотложной загрузки
связанных данных и размещения их в модели представления.
C#
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s =>
s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
Метод принимает необязательные данные маршрута ( id ) и строку запроса
( courseID ), которые содержат значения идентификатора выбранного
преподавателя и выбранного курса. Параметры передаются гиперссылками Select
на странице.
Код начинается с создания экземпляра модели представления и помещения его в
список преподавателей. В коде задается безотложная загрузка для свойств
навигации Instructor.OfficeAssignment и Instructor.CourseAssignments . Вместе со
свойством CourseAssignments загружается свойство Course , с которым загружаются
свойства Enrollments и Department , а с каждой сущностью Enrollment загружается
свойство Student .
C#
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Так как для представления всегда требуется сущность OfficeAssignment ,
значительно эффективнее извлекать ее в том же запросе. Получение сущностей
Course необходимо при выборе преподавателя на веб-странице, таким образом
одиночный запрос окажется предпочтительнее нескольких запросов, только если
страница отображается с курсами чаще, чем без них.
В коде повторяются CourseAssignments и Course , так как требуется получить два
свойства из Course . Первая строка ThenInclude вызывает получение
CourseAssignment.Course , Course.Enrollments и Enrollment.Student .
Дополнительные сведения о том, как включить несколько уровней связанных
данных, можно узнать здесь.
C#
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
В этой точке кода вызов метода ThenInclude получал бы свойства навигации
Student , которые нам требуются. Но вызов Include начнет заново получать
свойства Instructor , поэтому нам придется еще раз выполнить
последовательность команд, указав в этот раз Course.Department вместо
Course.Enrollments .
C#
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Следующий код выполняется при выборе преподавателя. Выбранный
преподаватель извлекается из списка преподавателей в модели представления.
Затем из свойства навигации CourseAssignments этого преподавателя загружается
свойство модели представления Courses вместе с сущностями Course .
C#
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
Метод Where возвращает коллекцию, но с учетом переданных в метод условий в
данном случае возвращается только одна сущность Instructor. Метод Single
преобразует коллекцию в отдельную сущность Instructor , что позволяет получить
доступ к ее свойству CourseAssignments . Свойство CourseAssignments содержит
сущности CourseAssignment , из которых нам нужны только связанные сущности
Course .
Использовать метод коллекции Single можно, если известно, что коллекция
содержит только один элемент. Метод Single вызывает исключение, если
передаваемая ему коллекция пустая или содержит больше одного элемента.
Альтернативным вариантом является метод SingleOrDefault , который возвращает
значение по умолчанию (в данном случае null), если коллекция пуста. Однако в
этом случае это все равно приведет к исключению (из-за попытки найти свойство
Courses у указателя на null), и из сообщения об исключении нелегко будет понять
причину проблемы. При вызове метода Single вы можете также передать условие
Where вместо отдельного вызова метода Where :
C#
.Single(i => i.ID == id.Value)
вместо следующего кода:
C#
.Where(i => i.ID == id.Value).Single()
Далее, если был выбран курс, то он получается из списка курсов модели
представления. Затем из свойства навигации Enrollments этого курса получается
свойство модели представления Enrollments вместе с сущностями Enrollment.
C#
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Изменение представления Instructor Index
В Views/Instructors/Index.cshtml замените код шаблона следующим кодом:
Изменения выделены.
CSHTML
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a>
|
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-routeid="@item.ID">Details</a> |
<a asp-action="Delete" asp-routeid="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Мы внесли следующие изменения в существующий код:
Изменили класс модели на InstructorIndexData .
Изменили заголовок страницы с Index на Instructors.
Добавили столбец Office, отображающий item.OfficeAssignment.Location
только тогда, когда item.OfficeAssignment не равно null. (Так как здесь
отношение один к нулю или к одному, то связанной сущности
OfficeAssignment может не существовать.)
CSHTML
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
Добавили столбец Courses, отображающий курсы, которые ведет конкретный
преподаватель. Дополнительные сведения см. в разделе Явный перенос
строки статьи по синтаксису Razor.
Добавлен код, который по условию добавляет класс CSS Bootstrap к элементу
tr выбранного преподавателя. Этот класс задает цвет фона для выделенной
строки.
В каждой строке непосредственно перед другими ссылками добавили новую
гиперссылку с меткой Select, которая отправляет идентификатор выбранного
преподавателя в метод Index .
CSHTML
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Запустите приложение и выберите вкладку Instructors. На странице отображается
свойство Location связанных сущностей OfficeAssignment либо пустая ячейка
таблицы при отсутствии связанной сущности OfficeAssignment.
В файле Views/Instructors/Index.cshtml после закрывающего таблицу элемента (в
конце файла) добавьте следующий код. Этот код отображает список связанных с
преподавателем курсов, когда преподаватель выбран.
CSHTML
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == (int?)ViewData["CourseID"])
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID =
item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
Этот код считывает свойство Courses модели представления для отображения
списка курсов. Он также предоставляет гиперссылку Select, которая отправляет
идентификатор выбранного курса в метод действия Index .
Обновите страницу и выберите преподавателя. Вы увидите сетку, которая
отображает курсы, назначенные выбранному преподавателю, и для каждого курса
отобразится имя связанного факультета.
После только что добавленного блока кода добавьте следующий код. Он
отображает список студентов, которые зачислены на курс при выборе этого курса.
CSHTML
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
Этот код считывает свойство Enrollments модели представления для отображения
списка студентов, зачисленных на этот курс.
Снова обновите страницу и выберите преподавателя. Затем выберите курс, чтобы
увидеть список зачисленных студентов и их оценки.
Сведения о явной загрузке
При получении списка преподавателей в файле InstructorsController.cs мы
указали безотложную загрузку для свойства навигации CourseAssignments .
Пусть ожидается, что пользователи лишь изредка будут просматривать список
зачисления для выбранных преподавателя и курса. В этом случае, возможно,
потребуется загружать данные о зачислении, только когда они запрошены. Чтобы
продемонстрировать пример того, как выполнять явную загрузку, замените метод
Index следующим кодом, который удаляет безотложную загрузку для Enrollments и
загружает это свойство явно. Изменения в коде выделены.
C#
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s =>
s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID ==
courseID).Single();
await _context.Entry(selectedCourse).Collection(x =>
x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x =>
x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
В новом коде из блока, который получает сущности преподавателей, удален вызов
метода ThenInclude . Также удаляется и AsNoTracking . Если выбраны преподаватели
и курс, выделенный код получает сущности Enrollment выбранного курса и
сущности Student для каждой сущности Enrollment .
Запустите приложение, перейдите на страницу Instructors Index, и вы не увидите
никаких изменений, несмотря на то, что мы изменили способ получения данных.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Дополнительные сведения о загрузке связанных данных
" Создание страницы курсов
" Создание страницы преподавателей
" Дополнительные сведения о явной загрузке
В следующем учебнике описано, как обновить связанные данные.
Обновление связанных данных
Руководство. Обновление связанных
данных — ASP.NET MVC с помощью EF
Core
Статья • 28.01.2023 • Чтение занимает 16 мин
В предыдущем руководстве вы отобразили связанные данные. В этом руководстве
описано обновление связанных данных путем обновления полей внешнего ключа
и свойств навигации.
На следующих рисунках изображены некоторые из страниц, с которыми вы будете
работать.
В этом учебнике рассмотрены следующие задачи.
" Настройка страниц курсов
" Добавление страницы редактирования данных о преподавателях
" Добавление курсов на страницу редактирования
" Обновление страницы удаления
" Добавление расположения кабинета и курсов на страницу создания
Предварительные требования
Чтение связанных данных
Настройка страниц курсов
Создаваемая сущность Course должна иметь связь с существующей кафедрой.
Чтобы упростить эту задачу, шаблонный код включает методы контроллеров, а
также представления "Create" (Создание) и "Edit" (Редактирование) с
раскрывающимся списком для выбора кафедры. Раскрывающийся список задает
свойство внешнего ключа Course.DepartmentID , и это все, что нужно Entity
Framework для загрузки свойства навигации Department с соответствующей
сущностью Department . Вы будете использовать этот шаблонный код, немного его
изменив, чтобы добавить обработку ошибок и сортировку раскрывающегося
списка.
В CoursesController.cs удалите методы Create и Edit и замените их следующим
кодом:
C#
public IActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
if (ModelState.IsValid)
{
_context.Add(course);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
C#
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var course = await _context.Courses
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
C#
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var courseToUpdate = await _context.Courses
.FirstOrDefaultAsync(c => c.CourseID == id);
if (await TryUpdateModelAsync<Course>(courseToUpdate,
"",
c => c.Credits, c => c.DepartmentID, c => c.Title))
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}
После метода HttpPost Edit создайте метод, загружающий сведения о кафедре для
раскрывающегося списка.
C#
private void PopulateDepartmentsDropDownList(object selectedDepartment =
null)
{
var departmentsQuery = from d in _context.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(),
"DepartmentID", "Name", selectedDepartment);
}
Метод PopulateDepartmentsDropDownList возвращает список всех кафедр,
отсортированных по имени, создает коллекцию SelectList для раскрывающегося
списка и передает ее в представление в ViewBag . Этот метод принимает
необязательный параметр selectedDepartment , позволяющий вызывающему коду
указать элемент, который будет выбран при отрисовке раскрывающегося списка.
Представление передаст имя "DepartmentID" во вспомогательную функцию тегов
<select> , после чего ей станет известно, что нужно искать в объекте ViewBag
коллекцию SelectList с именем "DepartmentID".
Метод HttpGet Create вызывает метод PopulateDepartmentsDropDownList без
установки выбранного элемента, так как кафедра для нового курса еще не задана:
C#
public IActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
Метод HttpGet Edit задает выбранный элемент на основе идентификатора
кафедры, который уже назначен редактируемому курсу:
C#
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var course = await _context.Courses
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
Методы HttpPost для Create и Edit также содержат код, который задает
выбранный элемент, когда они повторно отображают страницу после ошибки. Это
гарантирует, что при повторном отображении страницы для вывода сообщения об
ошибке сохраняется выбор кафедры.
Добавление .AsNoTrackin в методы Details и Delete
Чтобы оптимизировать производительность страниц "Details" (Сведения) и "Delete"
(Удаление) курса, добавьте вызовы AsNoTracking в методы Details и HttpGet
Delete .
C#
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
return View(course);
}
C#
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.CourseID == id);
if (course == null)
{
return NotFound();
}
return View(course);
}
Изменение представлений курса
Во Views/Courses/Create.cshtml добавьте параметр "Select Department" (Выбрать
кафедру) в раскрывающийся список Department (Кафедра), измените заголовок с
DepartmentID на Department и добавьте сообщение о проверке.
CSHTML
<div class="form-group">
<label asp-for="Department" class="control-label"></label>
<select asp-for="DepartmentID" class="form-control" aspitems="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>
<span asp-validation-for="DepartmentID" class="text-danger" />
</div>
Во Views/Courses/Edit.cshtml внесите для поля "Department" (Кафедра) изменение,
аналогичное внесенному в Create.cshtml .
Кроме того, добавьте во Views/Courses/Edit.cshtml поле номера курса перед полем
Title (Название). Так как номер курса является первичным ключом, он
отображается, но не может быть изменен.
CSHTML
<div class="form-group">
<label asp-for="CourseID" class="control-label"></label>
<div>@Html.DisplayFor(model => model.CourseID)</div>
</div>
Представление "Edit" (Редактирование) уже содержит скрытое поле ( <input
type="hidden"> ) для номера курса. Добавление вспомогательной функции тегов
<label> не устраняет потребность в этом скрытом поле, так как не приводит к
включению номера курса в передаваемые данные, когда пользователь нажимает
кнопку Save (Сохранить) на странице Edit (Редактирование).
Во Views/Courses/Delete.cshtml добавьте поле номера курса в верхней части
страницы и измените идентификатор кафедры на ее имя.
CSHTML
@model ContosoUniversity.Models.Course
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Course</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.CourseID)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.CourseID)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Title)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Credits)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Credits)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Department)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Department.Name)
</dd>
</dl>
<form asp-action="Delete">
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
Во Views/Courses/Details.cshtml внесите изменение, аналогичное внесенному в
Delete.cshtml .
Тестирование страниц курса
Запустите приложение, выберите вкладку Courses (Курсы), щелкните Create New
(Создать) и введите данные для нового курса:
Нажмите кнопку Создать. Отображается страница индекса курсов, где в список
добавлен новый курс. Название кафедры в списке страницы индекса поступает из
свойства навигации, показывая, что связь установлена правильно.
Нажмите кнопку Edit (Изменить) на странице индекса курсов.
Измените данные на странице и нажмите кнопку Save (Сохранить). Отображается
страница индекса курсов с обновленными данными о курсах.
Добавление страницы редактирования
данных о преподавателях
При редактировании записи преподавателя может потребоваться обновить
назначенный преподавателю кабинет. Сущность Instructor имеет связь "один к
нулю или к одному" с сущностью OfficeAssignment , что означает, что код должен
обрабатывать следующие ситуации.
Если пользователь сбрасывает назначение кабинета, которое изначально
имело некоторое значение, удалите сущность OfficeAssignment .
Если пользователь вводит значение для назначения кабинета, которое
изначально было пустым, создайте сущность OfficeAssignment .
Если пользователь изменяет значение для назначения кабинета, измените
значение в существующей сущности OfficeAssignment .
Обновление контроллера преподавателей
В InstructorsController.cs измените код метода HttpGet Edit , чтобы он загружал
свойство навигации OfficeAssignment сущности Instructor и вызывал AsNoTracking :
C#
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var instructor = await _context.Instructors
.Include(i => i.OfficeAssignment)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
return View(instructor);
}
Замените метод HttpPost Edit следующим кодом, чтобы обрабатывать обновления
назначения кабинета:
C#
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
if (id == null)
{
return NotFound();
}
var instructorToUpdate = await _context.Instructors
.Include(i => i.OfficeAssignment)
.FirstOrDefaultAsync(s => s.ID == id);
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
{
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
return View(instructorToUpdate);
}
Этот код выполняет следующее:
Изменяет имя метода на EditPost , так как сигнатура теперь аналогична
методу HttpGet Edit (атрибут ActionName указывает, что URL-адрес /Edit/ попрежнему используется).
Получает текущую сущность Instructor из базы данных, используя
безотложную загрузку для свойства навигации OfficeAssignment . Это
аналогично тому, что вы сделали в методе HttpGet Edit .
Обновляет извлеченную сущность Instructor , используя значения из
связывателя модели. Перегрузка TryUpdateModel позволяет объявить
включаемые свойства. Это защищает от чрезмерной передачи данных, как
описано во втором руководстве.
C#
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
Если расположение кабинета отсутствует, задает для свойства
Instructor.OfficeAssignment значение NULL, что приводит к удалению
связанной строки в таблице OfficeAssignment .
C#
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Locatio
n))
{
instructorToUpdate.OfficeAssignment = null;
}
Сохраняет изменения в базу данных.
Обновление представления редактирования
преподавателя
В конце Views/Instructors/Edit.cshtml , перед кнопкой Save (Сохранить), добавьте
новое поле для редактирования расположения кабинета:
CSHTML
<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label">
</label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger"
/>
</div>
Запустите приложение, выберите вкладку Instructors (Преподаватели), а затем
щелкните Edit (Изменить) для преподавателя. Измените значение Office Location
(Расположение кабинета) и нажмите кнопку Save (Сохранить).
Добавление курсов на страницу
редактирования
Преподаватели могут вести любое число курсов. Теперь вы усовершенствуете
страницу редактирования преподавателя, добавив возможность изменять
назначения курсов с помощью группы флажков, как показано на следующем
снимке экрана.
Между сущностями Course и Instructor действует связь "многие ко многим". Для
добавления и удаления связей можно добавлять сущности в список объединенного
набора сущностей CourseAssignments и удалять их из него.
Элементы пользовательского интерфейса, позволяющие изменять назначенные
преподавателю курсы, представляют собой группу флажков. Отображается флажок
для каждого курса в базе данных, и флажки установлены для тех курсов, которые
назначены текущему преподавателю. Пользователь может устанавливать и снимать
флажки, изменяя назначения курсов. Если бы количество курсов было значительно
больше, возможно, вам потребовалось бы использовать другой метод
отображения данных в этом представлении, но вы бы использовали тот же самый
способ управления сущностью объединения для создания и удаления связей.
Обновление контроллера преподавателей
Чтобы указать в представлении данные для списка флажков, используйте класс
моделей представления.
Создайте в папке SchoolViewModels файл AssignedCourseData.cs и замените
существующий код следующим:
C#
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
В файле InstructorsController.cs замените код метода HttpGet Edit приведенным
ниже кодом. Изменения выделены.
C#
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var instructor = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == id);
if (instructor == null)
{
return NotFound();
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = _context.Courses;
var instructorCourses = new HashSet<int>
(instructor.CourseAssignments.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewData["Courses"] = viewModel;
}
Этот код добавляет безотложную загрузку для свойства навигации Courses и
вызывает новый метод PopulateAssignedCourseData , который предоставляет
сведения для массива флажков, используя класс моделей представления
AssignedCourseData .
Код в методе PopulateAssignedCourseData считывает все сущности Course , чтобы
загрузить список курсов, используя класс модели представления. Для каждого
курса код проверяет, существует ли этот курс в свойстве навигации Courses
преподавателя. Чтобы создать эффективную подстановку при проверке того,
назначен ли курс преподавателю, назначаемые курсы помещаются в коллекцию
HashSet . У курсов, назначенных преподавателю, для свойства Assigned задается
значение true. Представление будет использовать это свойство, чтобы определить,
какие флажки нужно отображать как выбранные. Наконец, список передается в
представление в ViewData .
Добавьте код, выполняемый, когда пользователь нажимает кнопку Save
(Сохранить). Замените метод EditPost на следующий код и добавьте новый метод,
который обновляет свойство навигации Courses для сущности Instructor.
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
if (id == null)
{
return NotFound();
}
var instructorToUpdate = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.FirstOrDefaultAsync(m => m.ID == id);
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i =>
i.OfficeAssignment))
{
if
(String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction(nameof(Index));
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}
C#
private void UpdateInstructorCourses(string[] selectedCourses, Instructor
instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
Сигнатура метода теперь отличается от метода HttpGet Edit , поэтому имя метода
изменяется с EditPost обратно на Edit .
Так как представление не содержит коллекцию сущностей Course, связыватель
модели не может автоматически обновить свойство навигации CourseAssignments .
Вместо использования связывателя модели для обновления свойства навигации
CourseAssignments вы делаете это в новом методе UpdateInstructorCourses . Поэтому
нужно исключить свойство CourseAssignments из привязки модели. Это не требует
внесения никаких изменений в код, вызывающем TryUpdateModel , так как вы
используете перегрузку, требующую явного утверждения, а CourseAssignments
отсутствует в списке включений.
Если никаких флажков не выбрано, код в UpdateInstructorCourses инициализирует
свойство навигации CourseAssignments , используя пустую коллекцию, и возвращает
следующее.
C#
private void UpdateInstructorCourses(string[] selectedCourses, Instructor
instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
После этого код в цикле проходит по всем курсам в базе данных и сравнивает
каждый из них с теми, которые сейчас назначены преподавателю, в
противоположность тем, которые были выбраны в представлении. Чтобы
упростить эффективную подстановку, последние две коллекции хранятся в
объектах HashSet .
Если флажок для курса установлен, но курс отсутствует в свойстве навигации
Instructor.CourseAssignments , этот курс добавляется в коллекцию в свойстве
навигации.
C#
private void UpdateInstructorCourses(string[] selectedCourses, Instructor
instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
Если флажок для курса не установлен, но курс присутствует в свойстве навигации
Instructor.CourseAssignments , этот курс удаляется из свойства навигации.
C#
private void UpdateInstructorCourses(string[] selectedCourses, Instructor
instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.CourseAssignments.Select(c =>
c.Course.CourseID));
foreach (var course in _context.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.CourseAssignments.Add(new
CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID =
course.CourseID });
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
CourseAssignment courseToRemove =
instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID ==
course.CourseID);
_context.Remove(courseToRemove);
}
}
}
}
Обновление представлений преподавателя
В файле Views/Instructors/Edit.cshtml добавьте поле Courses (Курсы) с массивом
флажков, добавив приведенный ниже код сразу после элементов div для поля
Office (Кабинет) и перед элементом div для кнопки Save (Сохранить).
7 Примечание
При вставке кода в Visual Studio разрывы строк могут поменяться, нарушая
код. Если код выглядит иначе после вставки, нажмите клавиши CTRL + Z один
раз для отмены автоматического форматирования. Это исправляет разрывы
строк, благодаря чему код приобретает показанный здесь вид. Выравнивать
отступы необязательно, однако строки @:</tr><tr> , @:<td> , @:</td> и @:</tr>
должны находиться на одной строке, как показано здесь. В противном случае
возникает ошибка времени выполнения. Выделите блок нового кода и три
раза нажмите клавишу TAB, чтобы выровнять его с существующим кодом. Эта
проблема исправлена в Visual Studio 2019.
CSHTML
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;
foreach (var course in courses)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
Этот код создает таблицу HTML с тремя столбцами. Каждый столбец содержит
флажок, за которым следует подпись с номером и названием курса. Все флажки
имеют одно имя (selectedCourses), уведомляющее связыватель модели, что их
следует рассматривать как группу. Атрибуту значения для каждого флажка
присваивается значение CourseID . При публикации страницы связыватель модели
передает контроллеру массив, содержащий значения CourseID только для
выбранных флажков.
Флажкам, назначенным преподавателю, заданы атрибуты checked (установлены),
поэтому при первичной отрисовке флажков курсов они отображаются
установленными.
Запустите приложение, выберите вкладку Instructors (Преподаватели), а затем
щелкните Edit (Изменить) для преподавателя, чтобы открыть страницу Edit
(Редактирование).
Измените некоторые назначения курсов и нажмите кнопку "Save" (Сохранить).
Вносимые вами изменения отражаются на странице индекса.
7 Примечание
Описываемый здесь подход к редактированию данных курсов для
преподавателя эффективен при ограниченном числе курсов. Для коллекций
большего размера следовало бы применять другой пользовательский
интерфейс и другой метод обновления.
Обновление страницы удаления
В файле InstructorsController.cs удалите метод DeleteConfirmed и вставьте вместо
него приведенный ниже код.
C#
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
Instructor instructor = await _context.Instructors
.Include(i => i.CourseAssignments)
.SingleAsync(i => i.ID == id);
var departments = await _context.Departments
.Where(d => d.InstructorID == id)
.ToListAsync();
departments.ForEach(d => d.InstructorID = null);
_context.Instructors.Remove(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
Этот код вносит следующие изменения:
Выполняет безотложную загрузку для свойства навигации CourseAssignments .
Вам нужно включить его, иначе EF не будет знать о связанных сущностях
CourseAssignment и не удалит их. Чтобы избежать необходимости считывать
их, можно настроить каскадное удаление в базе данных.
Если преподаватель, которого требуется удалить, назначен в качестве
администратора любой из кафедр, удаляется назначение преподавателя из
таких кафедр.
Добавление расположения кабинета и
курсов на страницу создания
В файле InstructorsController.cs удалите методы HttpGet и HttpPost Create и
вставьте вместо них следующий код:
C#
public IActionResult Create()
{
var instructor = new Instructor();
instructor.CourseAssignments = new List<CourseAssignment>();
PopulateAssignedCourseData(instructor);
return View();
}
// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult>
Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor
instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.CourseAssignments = new List<CourseAssignment>();
foreach (var course in selectedCourses)
{
var courseToAdd = new CourseAssignment { InstructorID =
instructor.ID, CourseID = int.Parse(course) };
instructor.CourseAssignments.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
_context.Add(instructor);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}
Этот код аналогичен коду для методов Edit , за исключением того, что изначально
никакие курсы не выбраны. Метод HttpGet Create вызывает метод
PopulateAssignedCourseData не потому, что могут быть выбраны курсы, а чтобы
предоставить пустую коллекцию для цикла foreach в представлении (в противном
случае код представления выдаст исключение пустой ссылки).
Метод HttpPost Create добавляет каждый выбранный курс в свойство навигации
CourseAssignments до того, как выполнить поиск ошибок проверки и добавить
нового преподавателя в базу данных. Курсы добавляются даже при наличии
ошибок модели, поэтому когда имеются такие ошибки (например, пользователь
ввел недопустимую дату) и страница отображается повторно с сообщением об
ошибке, все выбранные курсы восстанавливаются автоматически.
Обратите внимание, что для добавления курсов в свойство навигации
CourseAssignments нужно инициализировать это свойство как пустую коллекцию:
C#
instructor.CourseAssignments = new List<CourseAssignment>();
Это можно сделать не только в коде контроллера, но и в модели Instructor ,
изменив метод получения свойств для автоматического создания коллекции, если
она не существует, как показано в следующем примере:
C#
private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
get
{
return _courseAssignments ?? (_courseAssignments = new
List<CourseAssignment>());
}
set
{
_courseAssignments = value;
}
}
При подобном изменении свойства CourseAssignments можно удалить код явной
инициализации свойства в контроллере.
В файле Views/Instructor/Create.cshtml добавьте текстовое поле для расположения
кабинета и флажки для курсов перед кнопкой Submit (Отправить). Как и в случае со
страницей редактирования, исправьте форматирование, если Visual Studio
переформатирует код при вставке.
CSHTML
<div class="form-group">
<label asp-for="OfficeAssignment.Location" class="control-label">
</label>
<input asp-for="OfficeAssignment.Location" class="form-control" />
<span asp-validation-for="OfficeAssignment.Location" class="text-danger"
/>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses =
ViewBag.Courses;
foreach (var course in courses)
{
if (cnt++ % 3 == 0)
{
@:</tr><tr>
}
@:<td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ?
"checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@:</tr>
}
</table>
</div>
</div>
Проверьте работу, запустив приложение и создав преподавателя.
Обработка транзакций
Как описано в руководстве по CRUD, платформа Entity Framework реализует
транзакции неявно. Если вам требуется дополнительный контроль, например в
сценариях с операциями, выполняемыми в транзакции вне платформы Entity
Framework, ознакомьтесь с разделом Транзакции.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Настройка страниц курсов
" Добавление страницы редактирования преподавателей
" Добавление курсов на страницу редактирования
" Обновление страницы удаления
" Добавление расположения кабинета и курсов на страницу создания
В следующем учебнике описано, как обрабатывать конфликты параллелизма.
Обработка конфликтов параллелизма
Руководство по обработке
параллелизма — ASP.NET MVC с
помощью EF Core
Статья • 28.01.2023 • Чтение занимает 16 мин
В предыдущих учебниках вы узнали, как обновлять данные. Это руководство
описывает, как обрабатывать конфликты, когда несколько пользователей
одновременно изменяют одну сущность.
Вы создадите веб-страницы, которые работают с сущностью Department и
обрабатывают ошибки параллелизма. На следующих рисунках показаны страницы
"Edit" (Редактирование) и "Delete" (Удаление), включая некоторые сообщения,
которые отображаются при возникновении конфликта параллелизма.
В этом учебнике рассмотрены следующие задачи.
" Дополнительные сведения о конфликтах параллелизма
" Добавление свойства отслеживания
" Создание представлений и контроллера кафедр
" Обновление представления указателя
" Обновление методов редактирования
" Обновление представления редактирования
" Тестирование конфликтов параллелизма
" Обновление страницы удаления
" Обновление представлений Details и Create
Предварительные требования
Обновление связанных данных
Конфликты параллелизма
Конфликт параллелизма возникает, когда один пользователь отображает данные
сущности, чтобы изменить их, а другой пользователь обновляет данные той же
сущности до того, как изменение первого пользователя будет записано в базу
данных. Если не включить обнаружение таких конфликтов, то пользователь,
обновляющий базу данных последним, перезаписывает изменения другого
пользователя. Во многих приложениях такой риск допустим: при небольшом числе
пользователей или обновлений, а также в случае, если перезапись некоторых
изменений не является критической, стоимость реализации параллелизма может
перевесить его преимущества. В этом случае вам не нужно настраивать
приложение для обработки конфликтов параллелизма.
Пессимистичный параллелизм (блокировка)
Если приложению нужно предотвратить случайную потерю данных в сценариях
параллелизма, одним из способов сделать это являются блокировки базы данных.
Это называется пессимистичным параллелизмом. Например, перед чтением строки
из базы данных вы запрашиваете блокировку для доступа для обновления или
только для чтения. Если заблокировать строку для обновления, другие
пользователи не могут заблокировать ее для обновления или только для чтения,
так как получат копию данных, которые находятся в процессе изменения. Если
заблокировать строку только для чтения, другие пользователи также могут
заблокировать ее только для чтения, но не для обновления.
Управление блокировками имеет недостатки. Оно может оказаться сложным с
точки зрения программирования. Оно требует значительных ресурсов управления
базами данных, а также может вызвать проблемы с производительностью по мере
увеличения числа пользователей приложения. Поэтому не все системы управления
базами данных поддерживают пессимистичный параллелизм. В Entity Framework
Core нет встроенной поддержки этой функции, и данное руководство не
рассказывает, как ее реализовать.
Оптимистическая блокировка
Альтернативой пессимистичному параллелизму является оптимистичный
параллелизм (оптимистическая блокировка). Оптимистическая блокировка
допускает появление конфликтов параллелизма, а затем обрабатывает их
соответствующим образом. Например, Мария посещает страницу изменения
кафедры и изменяет бюджет кафедры английской языка с 350 000,00 USD на 0,00
USD.
Прежде чем Мария нажимает кнопку Save (Сохранить), Дмитрий заходит на ту же
страницу и изменяет значение в поле "Start Date" (Дата начала) с 9/1/2007 на
9/1/2013.
Сначала Мария нажимает кнопку Save (Сохранить) и видит свое изменение, когда
браузер возвращается на страницу индекса.
Затем Дмитрий нажимает кнопку Save (Сохранить) на странице редактирования,
где все еще отображается бюджет 350 000,00 USD. Дальнейший ход событий
определяется порядком обработки конфликтов параллелизма.
Некоторые параметры перечислены ниже:
Вы можете отслеживать, для какого свойства пользователь изменил и
обновил только соответствующие столбцы в базе данных.
В этом примере сценария данные не будут потеряны, так как эти два
пользователя обновляли разные свойства. Когда какой-либо пользователь
просмотрит кафедру английского языка в следующий раз, он увидит
изменения, внесенные как Марией, так и Дмитрием — дату начала 9/1/2013 и
бюджет в нуль долларов США. Этот метод обновления помогает снизить
число конфликтов, которые могут привести к потере данных, но не позволяет
избежать такой потери, когда конкурирующие изменения вносятся в одно
свойство сущности. То, работает ли Entity Framework в таком режиме, зависит
от того, как вы реализуете код обновления. В веб-приложении это часто
нецелесообразно, так как может потребоваться обрабатывать большой объем
состояний, чтобы отслеживать все исходные значения свойств для сущности, а
также новые значения. Обработка большого объема состояний может
повлиять на производительность приложения, так как требует ресурсов
сервера или должна быть включена непосредственно в веб-страницу
(например, в скрытые поля) или файл cookie.
Вы можете позволить изменению Дмитрия перезаписать изменение Марии.
Когда какой-либо пользователь просмотрит кафедру английского языка в
следующий раз, он увидит дату 9/1/2013 и восстановленное значение
350 000,00 USD. Такой подход называется победой клиента или сохранением
последнего внесенного изменения. (Все значения из клиента имеют приоритет
над данными в хранилище.) Как отмечено во введении к этому разделу, если
вы не пишете код для обработки параллелизма, она выполняется
автоматически.
Вы можете запретить обновление изменения Дмитрия в базе данных.
Как правило, следует отобразить сообщение об ошибке, показать текущее
состояние данных и позволить ему повторно применить свои изменения, если
они ему нужны. Это называется победой хранилища. (Значения в хранилище
имеют приоритет над данными, передаваемыми клиентом.) В этом
руководстве вы реализуете сценарий победы хранилища. Данный метод
гарантирует, что никакие изменения не перезаписываются без оповещения
пользователя о случившемся.
Обнаружение конфликтов параллелизма
Конфликты можно разрешать путем обработки исключений
DbConcurrencyException , выдаваемых Entity Framework. Чтобы определить, когда
именно нужно выдавать исключения, платформа Entity Framework должна быть в
состоянии обнаруживать конфликты. Поэтому нужно соответствующим образом
настроить базу данных и модель данных. Ниже приведены некоторые варианты
для реализации обнаружения конфликтов:
Включите в таблицу базы данных столбец отслеживания, который позволяет
определять, когда была изменена строка. Затем можно настроить Entity
Framework для включения этого столбца в предложение Where команд SQL
Update или Delete.
Типом данных для столбца отслеживания обычно является rowversion .
Значение rowversion является последовательным номером,
увеличивающимся при каждом обновлении строки. В команде Update или
Delete предложение Where содержит исходное значение столбца
отслеживания (исходную версию строки). Если обновляемая строка была
изменена другим пользователем, значение в столбце rowversion отличается
от исходного значения, поэтому оператору Update или Delete не удается
найти строку для обновления из-за предложения Where. Когда платформа
Entity Framework обнаруживает, что ни одна из строк не была обновлена с
помощью команды Update или Delete (то есть число затронутых строк равно
нулю), она интерпретирует это как конфликт параллелизма.
Настройте Entity Framework для включения исходного значения каждого
столбца таблицы в предложение Where команд Update и Delete.
Как и в случае с первым вариантом, если с момента первого чтения в строке
что-либо изменилось, предложение Where не возвращает строку для
обновления, что платформа Entity Framework интерпретирует как конфликт
параллелизма. Для таблиц базы данных со множеством столбцов этот подход
может привести к очень большим предложениям Where и потребовать
обрабатывать большой объем состояний. Как было указано ранее,
обслуживание большого объема состояний может негативно повлиять на
производительность приложения. Поэтому в общем случае данный подход не
рекомендуется, кроме того, он не применяется и в этом руководстве.
Если вы хотите реализовать этот подход к параллелизму, нужно пометить все
свойства, не относящиеся к первичному ключу, в сущности, где требуется
отслеживать параллелизм, добавив к ним атрибут ConcurrencyCheck . Это
изменение позволяет Entity Framework включить все столбцы в предложение
Where SQL операторов Update и Delete.
В оставшейся части этого руководства вам предстоит добавить свойство
отслеживания rowversion в сущность Department, создать контроллер и
представления, а также проверить правильность работы решения.
Добавление свойства отслеживания
В Models/Department.cs добавьте свойство отслеживания RowVersion:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Атрибут Timestamp указывает, что этот столбец будет включен в предложение
Where команд Update и Delete, отправляемых в базу данных. Этот атрибут
называется Timestamp , так как предыдущие версии SQL Server использовали тип
данных timestamp SQL, пока его не сменил rowversion . Тип .NET для rowversion —
это массив байтов.
Если вы предпочитаете использовать текучий API, можно воспользоваться методом
IsConcurrencyToken (в Data/SchoolContext.cs ), чтобы указать свойство
отслеживания, как показано в следующем примере:
C#
modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();
Добавив свойство, вы изменили модель базы данных, поэтому нужно выполнить
еще одну миграцию.
Сохраните изменения, выполните сборку проекта и введите в командном окне
следующие команды:
Интерфейс командной строки.NET
dotnet ef migrations add RowVersion
Интерфейс командной строки.NET
dotnet ef database update
Создание представлений и контроллера
кафедр
Сформируйте шаблоны для контроллера кафедр и представлений, как делали это
раньше для учащихся, курсов и преподавателей.
В файле DepartmentsController.cs измените все четыре экземпляра "FirstMidName"
на "FullName", чтобы раскрывающиеся списки для администратора кафедры
содержали полное имя преподавателя, а не только фамилию.
C#
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID",
"FullName", department.InstructorID);
Обновление представления указателя
Подсистема формирования шаблонов создала столбец RowVersion в представлении
индекса, однако это поле не должно отображаться.
Замените код в Views/Departments/Index.cshtml следующим:
CSHTML
@model IEnumerable<ContosoUniversity.Models.Department>
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Administrator)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Budget)
</td>
<td>
@Html.DisplayFor(modelItem => item.StartDate)
</td>
<td>
@Html.DisplayFor(modelItem =>
item.Administrator.FullName)
</td>
<td>
<a asp-action="Edit" asp-routeid="@item.DepartmentID">Edit</a> |
<a asp-action="Details" asp-routeid="@item.DepartmentID">Details</a> |
<a asp-action="Delete" asp-routeid="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Он изменяет заголовок на "Departments" (Кафедры), удаляет столбец RowVersion и
отображает администратору имя и фамилию, а не только имя.
Обновление методов редактирования
Добавьте AsNoTracking в оба метода — HttpGet Edit и Details . В методе HttpGet
Edit добавьте безотложную загрузку для администратора.
C#
var department = await _context.Departments
.Include(i => i.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
Замените существующий код в методе HttpPost Edit следующим кодом:
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, byte[] rowVersion)
{
if (id == null)
{
return NotFound();
}
var departmentToUpdate = await _context.Departments.Include(i =>
i.Administrator).FirstOrDefaultAsync(m => m.DepartmentID == id);
if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
await TryUpdateModelAsync(deletedDepartment);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another
user.");
ViewData["InstructorID"] = new SelectList(_context.Instructors,
"ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
}
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue
= rowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by
another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value:
{databaseValues.Name}");
}
if (databaseValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Budget", $"Current value:
{databaseValues.Budget:c}");
}
if (databaseValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("StartDate", $"Current value:
{databaseValues.StartDate:d}");
}
if (databaseValues.InstructorID !=
clientValues.InstructorID)
{
Instructor databaseInstructor = await
_context.Instructors.FirstOrDefaultAsync(i => i.ID ==
databaseValues.InstructorID);
ModelState.AddModelError("InstructorID", $"Current
value: {databaseInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty, "The record you
attempted to edit "
+ "was modified by another user after you got the
original value. The "
+ "edit operation was canceled and the current
values in the database "
+ "have been displayed. If you still want to edit
this record, click "
+ "the Save button again. Otherwise click the Back
to List hyperlink.");
departmentToUpdate.RowVersion =
(byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
}
}
}
ViewData["InstructorID"] = new SelectList(_context.Instructors, "ID",
"FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}
Код начинает пытаться считать кафедру для обновления. Если метод
FirstOrDefaultAsync возвращает значение null, кафедра была удалена другим
пользователем. В этом случае код использует переданные значения из формы для
создания сущности Department , чтобы страницу редактирования можно было
отобразить повторно с сообщением об ошибке. Как вариант, можно повторно не
создавать сущность Department , если вы выводите только сообщение об ошибке
без повторного отображения полей кафедры.
Представление сохраняет исходное значение RowVersion в скрытом поле, а данный
метод получает это значение в параметре rowVersion . Перед вызовом SaveChanges
нужно поместить это исходное значение свойства RowVersion в коллекцию
OriginalValues для сущности.
C#
_context.Entry(departmentToUpdate).Property("RowVersion").OriginalValue =
rowVersion;
Когда позднее Entity Framework создает команду SQL UPDATE, она будет содержать
предложение WHERE, которое ищет строку с исходным значением RowVersion . Если
команда UPDATE не затрагивает никакие строки (нет строк, имеющих исходное
значение RowVersion ), Entity Framework выдает
исключение DbUpdateConcurrencyException .
Код в блоке catch для этого исключения возвращает затронутую сущность
Department, имеющую обновленные значения из свойства Entries в объекте
исключения.
C#
var exceptionEntry = ex.Entries.Single();
Коллекция Entries будет иметь всего один объект EntityEntry . Вы можете
использовать его для получения новых значений, введенных пользователем, и
текущих значений в базе данных.
C#
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
Код добавляет настраиваемое сообщение об ошибке для каждого столбца, для
которого значения базы данных отличаются от введенных пользователем на
странице "Edit" (Редактирование) (здесь для краткости показано всего одно поле).
C#
var databaseValues = (Department)databaseEntry.ToObject();
if (databaseValues.Name != clientValues.Name)
{
ModelState.AddModelError("Name", $"Current value:
{databaseValues.Name}");
Наконец, код задает для RowVersion объекта departmentToUpdate новое значение,
полученное из базы данных. Это новое значение RowVersion будет сохранено в
скрытом поле при повторном отображении страницы "Edit" (Редактирование).
Когда пользователь в следующий раз нажимает кнопку Save (Сохранить),
перехватываются только те ошибки параллелизма, которые возникли с момента
повторного отображения страницы "Edit" (Редактирование).
C#
departmentToUpdate.RowVersion = (byte[])databaseValues.RowVersion;
ModelState.Remove("RowVersion");
Оператор ModelState.Remove является обязательным, так как ModelState имеет
старое значение RowVersion . На представлении значение ModelState для поля
имеет приоритет над значениями свойств модели, если они присутствуют вместе.
Обновление представления редактирования
В Views/Departments/Edit.cshtml внесите следующие изменения:
Добавьте скрытое поле для сохранения значения свойства RowVersion сразу
после скрытого поля для свойства DepartmentID .
Добавьте пункт "Select Administrator" (Выбрать администратора) в
раскрывающийся список.
CSHTML
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" aspitems="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
<span asp-validation-for="InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Тестирование конфликтов параллелизма
Запустите приложение и перейдите на страницу индекса кафедр. Щелкните правой
кнопкой мыши гиперссылку Edit (Изменить) для кафедры английского языка и
выберите пункт Открыть на новой вкладке, а затем щелкните гиперссылку Edit
(Изменить) для этой кафедры. Теперь на обеих вкладках браузера отображаются
одинаковые сведения.
Измените поле на первой вкладке браузера и нажмите кнопку Save (Сохранить).
В браузере отображается страница индекса с измененным значением.
Измените поле на второй вкладке браузера.
Нажмите кнопку Сохранить. Отображается сообщение об ошибке:
Снова нажмите кнопку Save (Сохранить). Сохраняется значение, введенное на
второй вкладке браузера. Сохраненные значения отображаются при открытии
страницы индекса.
Обновление страницы удаления
Для страницы "Delete" (Удаление) платформа Entity Framework обнаруживает
конфликты параллелизма, вызванные схожим изменением кафедры. Когда метод
HttpGet Delete отображает представление подтверждения, оно содержит исходное
значение RowVersion в скрытом поле. Затем это значение становится доступным
для метода HttpPost Delete , вызываемого при подтверждении удаления
пользователем. Когда Entity Framework создает команду SQL DELETE, она включает
предложение WHERE с исходным значением RowVersion . Если команда не
затрагивает ни одной строки (подразумевается изменение строки после
отображения страницы подтверждения удаления), возникает исключение
параллелизма и вызывается метод HttpGet Delete , у которого для флага ошибки
установлено значение true, чтобы повторно отобразить страницу подтверждения с
сообщением об ошибке. Отсутствие затронутых строк также может быть вызвано
тем, что строка была удалена другим пользователем, и в этом случае сообщение об
ошибке не отображается.
Обновление методов Delete в контроллере кафедр
В файле DepartmentsController.cs замените код метода HttpGet Delete
приведенным ниже кодом:
C#
public async Task<IActionResult> Delete(int? id, bool? concurrencyError)
{
if (id == null)
{
return NotFound();
}
var department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction(nameof(Index));
}
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ViewData["ConcurrencyErrorMessage"] = "The record you attempted to
delete "
+ "was modified by another user after you got the original
values. "
+ "The delete operation was canceled and the current values in
the "
+ "database have been displayed. If you still want to delete
this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
}
return View(department);
}
Этот метод принимает необязательный параметр, который указывает,
отображается ли страница повторно после ошибки параллелизма. Если этот флаг
имеет значение true, а указанная кафедра больше не существует, она была удалена
другим пользователем. В этом случае код выполняет перенаправление на страницу
индекса. Если этот флаг имеет значение true, а кафедра существует, она была
изменена другим пользователем. В этом случае код отправляет сообщение об
ошибке в представление, используя ViewData .
Замените код в методе HttpPost Delete (с именем DeleteConfirmed ) следующим
кодом:
C#
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(Department department)
{
try
{
if (await _context.Departments.AnyAsync(m => m.DepartmentID ==
department.DepartmentID))
{
_context.Departments.Remove(department);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
catch (DbUpdateConcurrencyException /* ex */)
{
//Log the error (uncomment ex variable name and write a log.)
return RedirectToAction(nameof(Delete), new { concurrencyError =
true, id = department.DepartmentID });
}
}
В шаблонном коде, который вы только что заменили, этот метод принимал только
идентификатор записи:
C#
public async Task<IActionResult> DeleteConfirmed(int id)
Вы изменили этот параметр на экземпляр сущности Department , созданный
связывателем модели. Это предоставляет EF доступ к значению свойства
RowVersion в дополнение к ключу записи.
C#
public async Task<IActionResult> Delete(Department department)
Вы также изменили имя метода действия с DeleteConfirmed на Delete . Шаблонный
код использовал имя DeleteConfirmed , чтобы предоставить методу HttpPost
уникальную сигнатуру. (Среда CLR требует, чтобы перегруженные методы имели
разные параметры метода.) Теперь, когда сигнатуры являются уникальными,
можно придерживаться соглашения MVC и использовать одинаковое имя для
методов delete HttpGet и HttpPost.
Если кафедра уже удалена, метод AnyAsync возвращает значение false, а
приложение просто возвращается к методу Index.
При перехвате ошибки параллелизма код повторно отображает страницу
подтверждения удаления и предоставляет флаг, указывающий, что нужно
отобразить сообщение об ошибке параллелизма.
Обновление представления удаления
В файле Views/Departments/Delete.cshtml замените шаблонный код приведенным
ниже кодом, который добавляет поле сообщения об ошибке и скрытые поля для
свойств DepartmentID и RowVersion. Изменения выделены.
CSHTML
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@ViewData["ConcurrencyErrorMessage"]</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
<form asp-action="Delete">
<input type="hidden" asp-for="DepartmentID" />
<input type="hidden" asp-for="RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
Этот код вносит следующие изменения:
Добавляет сообщение об ошибке между заголовками h2 и h3 .
Заменяет FirstMidName на FullName в поле Administrator (Администратор).
Удаляет поле RowVersion.
Добавляет скрытое поле для свойства RowVersion .
Запустите приложение и перейдите на страницу индекса кафедр. Щелкните правой
кнопкой мыши гиперссылку Delete (Удалить) для кафедры английского языка и
выберите пункт Открыть на новой вкладке, а затем на первой вкладке щелкните
гиперссылку Edit (Изменить) для этой кафедры.
В первом окне измените одно из значений и нажмите кнопку Save (Сохранить):
На второй вкладке нажмите кнопку Delete (Удалить). Вы видите сообщение об
ошибке параллелизма, а значения кафедры обновляются с использованием
актуальных сведений из базы данных.
Если нажать кнопку Delete (Удалить) еще раз, вы будете перенаправлены на
страницу индекса, которая показывает, что кафедра была удалена.
Обновление представлений Details и Create
При необходимости вы можете очистить шаблонный код в представлениях Details
и Create.
Замените код в Views/Departments/Details.cshtml , чтобы удалить столбец
RowVersion и отобразить полное имя администратора.
CSHTML
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Department</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Budget)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Budget)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.StartDate)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.StartDate)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Administrator)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Administrator.FullName)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.DepartmentID">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
Замените код в Views/Departments/Create.cshtml , чтобы добавить параметр "Select"
(Выбрать) в раскрывающийся список.
CSHTML
@model ContosoUniversity.Models.Department
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger">
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Budget" class="control-label"></label>
<input asp-for="Budget" class="form-control" />
<span asp-validation-for="Budget" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control" />
<span asp-validation-for="StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label asp-for="InstructorID" class="control-label"></label>
<select asp-for="InstructorID" class="form-control" aspitems="ViewBag.InstructorID">
<option value="">-- Select Administrator --</option>
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default"
/>
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Дополнительные ресурсы
Дополнительные сведения об обработке параллелизма см. в EF Coreразделе
"Конфликты параллелизма".
Дальнейшие действия
В этом учебнике рассмотрены следующие задачи.
" Дополнительные сведения о конфликтах параллелизма
" Добавление свойства отслеживания
" Создание представлений и контроллера кафедр
" Обновление представления указателя
" Обновление методов редактирования
" Обновление представления редактирования
" Тестирование конфликтов параллелизма
" Обновление страницы удаления
" Обновление представлений сведений и создания
В следующем учебнике описано, как реализовать наследование "одна таблица на
иерархию" для сущностей Instructor и Student.
Далее: Реализация наследования типа "одна таблица на иерархию"
Руководство. Реализация
наследования — ASP.NET MVC с
помощью EF Core
Статья • 28.01.2023 • Чтение занимает 7 мин
В предыдущем учебнике была показана обработка исключений параллелизма. В
этом учебнике демонстрируется, как реализовать наследование в модели данных.
В объектно-ориентированном программировании наследование применяется для
оптимизации повторного использования кода. В рамках этого учебника вы
измените классы Instructor и Student таким образом, чтобы они были
производными от базового класса Person , который содержит общие свойства для
преподавателей и учащихся, такие как LastName . Изменения вносятся в коде, а не
на веб-страницах, и автоматически отражаются в базе данных.
В этом учебнике рассмотрены следующие задачи.
" Сопоставление наследования с базой данных
" Создание класса Person
" Обновление Instructor и Student
" Добавление Person в модель
" Создание и обновление миграций
" Тестирование реализации
Предварительные требования
Обработка параллелизма
Сопоставление наследования с базой
данных
Классы Instructor и Student в модели данных School имеют несколько идентичных
свойств:
Предположим, что вам требуется исключить повторяющийся код для свойств,
которые являются общими для сущностей Instructor и Student . Кроме того, вам
может потребоваться написать службу, которая может форматировать имена как
преподавателей, так и учащихся. В таких сценариях можно создать базовый класс
Person , который содержит только общие свойства, а затем унаследовать от него
классы Instructor и Student , как показано на следующем рисунке:
Структура наследования может быть представлена в базе данных несколькими
способами. У вас может быть таблица Person , содержащая одновременно
информацию о преподавателях и учащихся. Некоторые столбцы могут относиться
только к преподавателям (HireDate), некоторые только к учащимся
(EnrollmentDate), а некоторые одновременно к обеим сущностям (LastName,
FirstName). Как правило, используется столбец дискриминатора, который указывает
на тип, представленный соответствующей строкой. Например, в столбце
дискриминатора может указываться значение "Instructor" для преподавателей и
"Student" для учащихся.
Такая модель, описывающая формирование структуры наследования сущностей на
основе одной таблицы базы данных, называется наследованием типа одна
таблица на иерархию (TPH) .
В качестве альтернативы можно создать базу данных, которая будет иметь
приближенный к структуре наследования вид. Например, можно хранить в таблице
Person только поля с именами и создать отдельные таблицы Instructor и Student с
полями дат.
2 Предупреждение
Таблица на тип (TPT) не поддерживается EF Core 3.x, однако она реализована в
EF Core версии 5.0.
Такая модель создания таблицы базы данных для каждого класса сущности
называется наследованием типа одна таблица на тип (TPT) .
Кроме того, можно сопоставить все не являющиеся абстрактными типы с
отдельными таблицами. Все свойства класса, включая унаследованные,
сопоставляются со столбцами в соответствующей таблице. Такая модель
называется наследованием типа одна таблица на конкретный класс (TPC) . Если
реализовать наследование типа "одна таблица на конкретный класс" для
показанных выше классов Person , Student и Instructor , таблицы Student и
Instructor после реализации наследования будут выглядеть так же, как и до этого.
Модели наследования "одна таблица на иерархию" и "одна таблица на конкретный
класс", как правило, обеспечивают более высокую производительность по
сравнению с моделью "одна таблица на тип", поскольку при использовании
последней могут выполняться сложные запросы на соединение.
В этом учебнике демонстрируется реализация модели наследования "одна таблица
на иерархию". Платформа Entity Framework поддерживает только модель
наследования "одна таблица на иерархию". Вам необходимо создать класс Person ,
изменить классы Instructor и Student так, чтобы они были производными от
класса Person , добавить новый класс в DbContext , после чего создать миграцию.
 Совет
Перед внесением изменений рекомендуется сохранить копию проекта. В этом
случае, если возникнут проблемы, вы сможете вернуться к сохраненному
проекту вместо того, чтобы отменять выполненные в рамках этого учебника
действия или начинать всю серию сначала.
Создание класса Person
В папке Models создайте файл Person.cs и замените код шаблона следующим:
C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public abstract class Person
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than
50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}
Обновление Instructor и Student
В файле Instructor.cs измените класс Instructor так, чтобы он был производным от
класса Person, и удалите поля ключа и имени. Код будет выглядеть следующим
образом:
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
Выполните те же изменения в файле Student.cs .
C#
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel.DataAnnotations;
System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Добавление Person в модель
Добавьте тип сущности Person в файл SchoolContext.cs . Новые строки выделены.
C#
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) :
base(options)
{
}
public
public
public
public
public
public
public
public
DbSet<Course> Courses { get; set; }
DbSet<Enrollment> Enrollments { get; set;
DbSet<Student> Students { get; set; }
DbSet<Department> Departments { get; set;
DbSet<Instructor> Instructors { get; set;
DbSet<OfficeAssignment> OfficeAssignments
DbSet<CourseAssignment> CourseAssignments
DbSet<Person> People { get; set; }
}
}
}
{ get; set; }
{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>
().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>
().ToTable("CourseAssignment");
modelBuilder.Entity<Person>().ToTable("Person");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
Это все, что требуется платформе Entity Framework для настройки наследования
типа "одна таблица на иерархию". Как видно, после обновления базы данных в ней
будет присутствовать таблица Person вместо таблиц Student и Instructor.
Создание и обновление миграций
Сохраните изменения и выполните сборку проекта. Затем откройте командное
окно в папке проекта и введите следующую команду:
Интерфейс командной строки.NET
dotnet ef migrations add Inheritance
На этом этапе не выполняйте команду database update . Ее выполнение приведет к
потере данных, поскольку будет удалена таблица Instructor, а таблица Student будет
переименована в Person. Для сохранения существующих данных потребуется
настраиваемый код.
Откройте Migrations/<timestamp>_Inheritance.cs и замените метод Up следующим
кодом:
C#
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Enrollment_Student_StudentID",
table: "Enrollment");
migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table:
"Enrollment");
migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table:
"Person", nullable: true);
migrationBuilder.AddColumn<string>(name: "Discriminator", table:
"Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table:
"Person", nullable: true);
migrationBuilder.AddColumn<int>(name: "OldId", table: "Person",
nullable: true);
// Copy existing Student data into new Person table.
migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName,
HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName,
null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId
FROM dbo.Student");
// Fix up existing relationships to match new PK's.
migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID
FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator =
'Student')");
// Remove temporary key
migrationBuilder.DropColumn(name: "OldID", table: "Person");
migrationBuilder.DropTable(
name: "Student");
migrationBuilder.CreateIndex(
name: "IX_Enrollment_StudentID",
table: "Enrollment",
column: "StudentID");
migrationBuilder.AddForeignKey(
name: "FK_Enrollment_Person_StudentID",
table: "Enrollment",
column: "StudentID",
principalTable: "Person",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
}
Этот код выполняет следующие задачи по обновлению базы данных:
Удаляет ограничения внешнего ключа и индексы, которые указывают на
таблицу Student.
Переименовывает таблицу Instructor в Person и вносит изменения,
необходимые для сохранения в ней данных из таблицы Student:
Добавляет допускающий значения NULL тип EnrollmentDate для учащихся.
Добавляет столбец дискриминатора, который указывает, представляет ли
строка учащегося или преподавателя.
Изменяет тип HireDate на допускающий значения NULL, поскольку в строках
для учащихся не будет указываться дата приема на работу.
Добавляет временное поле, которое будет использоваться для обновления
внешних ключей, указывающих на учащихся. При копировании записей
учащихся в таблицу Person им назначаются новые значения первичного
ключа.
Копирует данные из таблицы Student в таблицу Person. При этом записям
учащихся назначаются новые значения первичного ключа.
Исправляет значения внешнего ключа, которые указывают на учащихся.
Повторно создает ограничения внешнего ключа и индексы, которые после
этого указывают на таблицу Person.
(Если вместо целочисленного типа первичного ключа используется GUID, значения
первичного ключа для записей учащихся изменять не потребуется и некоторые из
этих действий можно пропустить.)
Выполните команду database update :
Интерфейс командной строки.NET
dotnet ef database update
(В рабочей системе потребовалось бы внести соответствующие изменения в метод
Down , который может быть необходим для возврата к предыдущей версии базы
данных. В этом учебнике метод Down использоваться не будет.)
7 Примечание
При изменении схемы в базе, содержащей существующие данные, возможны
другие ошибки. Если вы получаете ошибки миграции, которые не удается
устранить, измените имя базы данных в строке подключения или удалите базу
данных. В новой базе не будет данных, которые требуется перенести, в
результате чего команда обновления базы данных с большей долей
вероятности завершится без ошибок. Чтобы удалить базу данных, используйте
средство SSOX или выполните команду database drop в интерфейсе командной
строки.
Тестирование реализации
Запустите приложение и попробуйте открыть различные страницы. Все работает
так же, как и раньше.
В обозревателе объектов SQL Server разверните узел Data
Connections/SchoolContext и затем Таблицы. Вы увидите, что вместо таблиц
Student и Instructor появилась таблица Person. Откройте таблицу Person в
конструкторе и убедитесь, что в ней представлены все столбцы, содержавшиеся в
таблицах Student и Instructor.
Щелкните таблицу Person правой кнопкой мыши и выберите команду Показать
данные таблицы, чтобы просмотреть столбец дискриминатора.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Дополнительные ресурсы
Дополнительные сведения о наследовании на платформе Entity Framework Core см.
в разделе Наследование.
Следующие шаги
В этом учебнике рассмотрены следующие задачи.
" Сопоставление наследования с базой данных
" Создание класса Person
" Обновление Instructor и Student
" Добавление Person в модель
" Создание и обновление миграций
" Тестирование реализации
В следующем учебнике описано, как использовать несколько сценариев
Entity Framework с расширенными возможностями.
Далее: Дополнительные разделы
Руководство. Дополнительные
сведения о расширенных сценариях
— ASP.NET MVC с EF Core
Статья • 28.01.2023 • Чтение занимает 12 мин
В предыдущем учебнике было реализовано наследование типа "одна таблица на
иерархию". В этом учебнике описываются некоторые расширенные возможности,
не относящиеся к базовой разработке веб-приложений ASP.NET Core,
использующих платформу Entity Framework Core.
В этом учебнике рассмотрены следующие задачи.
" Выполнение прямых SQL-запросов
" Вызов запроса для получения сущностей
" Вызов запроса для получения других типов
" Вызов запроса на обновление
" Изучение SQL-запросов
" Создание уровня абстракции
" Сведения об автоматическом обнаружении изменений
" Сведения о исходном коде EF Core и планах разработки
" Сведения об использовании динамических запросов LINQ для упрощения кода
Предварительные требования
Реализация наследования
Выполнение прямых SQL-запросов
Одним из преимуществ использования платформы Entity Framework является
возможность избежать слишком тесной привязки кода к конкретному способу
хранения данных. Это достигается путем автоматического создания запросов и
команд SQL, что позволяет упростить написание кода. Тем не менее в редких
случаях требуется выполнять созданные вручную SQL-запросы. Для таких
сценариев в API Entity Framework Code First включены методы, позволяющие
передавать команды SQL напрямую в базу данных. В версии 1.0 доступны
следующие параметры EF Core :
Использование метода DbSet.FromSql для запросов, которые возвращают
типы сущностей. Возвращаемые объекты должны иметь тип, ожидаемый
объектом DbSet , и автоматически отслеживаются контекстом базы данных,
кроме случаев, когда отслеживание отключено.
Использование Database.ExecuteSqlCommand для команд, не относящихся к
запросам.
Если вам необходимо выполнить запрос, который возвращает типы, не
являющиеся сущностями, можно использовать ADO.NET с подключением к базе
данных, предоставленным платформой EF. Возвращаемые данные не
отслеживаются контекстом базы данных, даже если вы используете этот метод для
извлечения типов сущностей.
Как и всегда при выполнении команд SQL в веб-приложении, необходимо
принимать меры предосторожности для защиты сайта от атак путем внедрения
кода SQL. Одним из способов защиты является применение параметризованных
запросов, которые гарантируют, что строки, отправляемые веб-страницей, не могут
быть интерпретированы как команды SQL. В рамках этого учебника вы будете
использовать параметризованные запросы при интеграции вводимых
пользователем данных в запрос.
Вызов запроса для получения сущностей
Класс DbSet<TEntity> предоставляет метод, который можно использовать для
выполнения запроса, возвращающего сущность типа TEntity . Чтобы увидеть, как
это работает, измените код в методе Details для контроллера Department.
В файле DepartmentsController.cs в методе Details замените код, извлекающий
отдел, вызовом метода FromSql , как показано ниже в выделенном коде:
C#
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}
Чтобы убедиться, что новый код работает правильно, выберите вкладку
Departments (Кафедры) и щелкните Details (Сведения) для одной из кафедр.
Вызов запроса для получения других типов
Ранее вы создали таблицу статистики учащихся на странице сведений, в которой
было показано число учащихся на каждую дату регистрации. Вы получали эти
данные из набора сущностей Students ( _context.Students ) и использовали LINQ,
чтобы спроецировать результаты в список объектов модели представления
EnrollmentDateGroup . Предположим, что вы хотите написать код SQL вместо
использования LINQ. Для этого вам необходимо выполнить SQL-запрос, который
возвращает объекты, не являющиеся сущностями. В EF Core версии 1.0 один из
способов сделать это — написать ADO.NET код и получить подключение к базе
данных из EF.
В HomeController.cs замените метод About следующим кодом:
C#
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount
"
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate =
reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}
Добавьте инструкцию using:
C#
using System.Data.Common;
Запустите приложение и перейдите на страницу About. На экран будут выведены те
же данные, что и ранее.
Вызов запроса на обновление
Предположим, что администраторам университета Suppose необходимо внести
глобальные изменения в базу данных, например изменить число зачетных баллов
для каждого курса. Поскольку в университете ведется множество курсов, будет
неэффективно извлекать их в виде сущностей и изменять по отдельности. В этом
разделе вы реализуете веб-страницу, на которой пользователь может задать
множитель, который будет применен к числу зачетных баллов для каждого курса,
после чего изменения будут внесены с помощью инструкции SQL UPDATE. Вебстраница должна выглядеть так, как показано на следующем рисунке:
В файле CoursesController.cs добавьте методы UpdateCourseCredits для HttpGet и
HttpPost:
C#
public IActionResult UpdateCourseCredits()
{
return View();
}
C#
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
if (multiplier != null)
{
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);
}
return View();
}
Когда контроллер обрабатывает запрос HttpGet, в ViewData["RowsAffected"] ничего
не возвращается, а в представлении отображается пустое текстовое поле и кнопка
отправки, как показано на предыдущем рисунке.
При нажатии кнопки Update (Обновить) вызывается метод HttpPost, а множителю
присваивается значение, введенное в текстовое поле. После этого код выполняет
SQL-запрос, который обновляет курсы и возвращает число затронутых строк в
представление в ViewData . После того как в представление передано значение
RowsAffected , оно отображает число обновленных строк.
В обозревателе решений щелкните правой кнопкой мыши папку Views/Courses и
выберите Добавить > Новый элемент.
В диалоговом окне Добавление нового элемента щелкните элемент ASP.NET Core
в разделе Установленные на панели слева, выберите Представление Razor и
присвойте новому представлению имя UpdateCourseCredits.cshtml .
В Views/Courses/UpdateCourseCredits.cshtml замените код шаблона следующим
кодом:
CSHTML
@{
ViewBag.Title = "UpdateCourseCredits";
}
<h2>Update Course Credits</h2>
@if (ViewData["RowsAffected"] == null)
{
<form asp-action="UpdateCourseCredits">
<div class="form-actions no-color">
<p>
Enter a number to multiply every course's credits by:
@Html.TextBox("multiplier")
</p>
<p>
<input type="submit" value="Update" class="btn btn-default"
/>
</p>
</div>
</form>
}
@if (ViewData["RowsAffected"] != null)
{
<p>
Number of rows updated: @ViewData["RowsAffected"]
</p>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
Выполните метод UpdateCourseCredits , выбрав вкладку Courses (Курсы), а затем
добавив "/UpdateCourseCredits" в конец URL-адреса в адресной строке браузера
(например, http://localhost:5813/Courses/UpdateCourseCredits ). Введите число в
текстовое поле:
Нажмите кнопку Обновить. Отобразится число обработанных строк:
Нажмите кнопку Back to List (Вернуться к списку), чтобы просмотреть список
курсов с измененным числом зачетных баллов.
Обратите внимание, что в рабочем коде необходимо всегда проверять
допустимость новых данных. В приведенном здесь упрощенном коде в результате
применения множителя могут получиться значения больше 5. (Свойство Credits
имеет атрибут [Range(0, 5)] .) Запрос на обновление будет выполнен, однако из-за
недопустимых данных в других частях системы, в которых число зачетных баллов
должно быть не больше 5, могут возникать неожиданные результаты.
Дополнительные сведения о необработанных SQL-запросах см. в разделе
Необработанные SQL-запросы.
Изучение SQL-запросов
В некоторых случаях полезно иметь возможность просмотреть фактические SQLзапросы, отправляемые в базу данных. Встроенные функции ведения журнала для
ASP.NET Core автоматически используются EF Core для записи журналов,
содержащих SQL для запросов и обновлений. В этом разделе приводятся
некоторые примеры ведения журналов кода SQL.
Откройте файл StudentsController.cs и установите в методе Details точку
останова на инструкции if (student == null) .
Запустите приложение в режиме отладки и перейдите на страницу Details
(Сведения) для учащегося.
Перейдите в окно вывода, в котором отображаются результаты отладки. В нем
будет показан запрос:
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed
DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text',
CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].
[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed
DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text',
CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID],
[s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID],
[e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] =
[e.Course].[CourseID]
INNER JOIN (
SELECT TOP(1) [s0].[ID]
FROM [Person] AS [s0]
WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]
Вы можете заметить удивительные результаты: код SQL выбирает до 2 строк
( TOP(2) ) из таблицы Person. Метод SingleOrDefaultAsync не разрешается в 1 строку
на сервере. Далее описывается, почему это происходит:
Если запрос возвращает несколько строк, метод возвращает значение NULL.
Чтобы определить, будет ли запрос возвращать несколько строк, платформа
EF проверяет, возвращаются ли как минимум 2 строки.
Обратите внимание, что вам необязательно использовать режим отладки и
доходить до точки останова, чтобы просмотреть содержимое журнала в окне
вывода. Это просто удобный способ остановить ведение журнала в тот момент,
когда вам нужно просмотреть выходные данные. Если не сделать этого, ведение
журнала продолжится и вам придется прокручивать окно до нужного места.
Создание уровня абстракции
Многие разработчики пишут код, реализующий шаблоны репозитория и единиц
работы, в качестве оболочки для кода, работающего с платформой Entity
Framework. Эти шаблоны позволяют создать уровень абстракции между уровнями
доступа к данным и бизнес-логики приложения. Реализация таких шаблонов
позволяет изолировать приложение от изменений в хранилище данных и
упрощает автоматическое модульное тестирование или разработку на основе
тестирования. Тем не менее написание дополнительного кода для реализации этих
шаблонов не всегда подходит для приложений, которые используют платформу EF.
Этому есть несколько причин:
Класс контекста EF сам по себе изолирует код от кода хранилища данных.
Класс контекста EF может выступать в качестве класса единиц работы для
обновления базы данных, которые выполняются с помощью EF.
Платформа EF предусматривает функции для реализации разработки на
основе тестирования, не требующие написания кода репозитория.
Дополнительные сведения о реализации шаблонов репозитория и единиц работы
см. в версии этой серии учебников для платформы Entity Framework 5.
Платформа Entity Framework Core реализует выполняющийся в памяти поставщик
базы данных, который может использоваться для тестирования. Дополнительные
сведения см. в разделе Тестирование с помощью InMemory.
Автоматическое обнаружение изменений
Платформа Entity Framework определяет, как была изменена сущность (и,
соответственно, какие обновления требуется отправить в базу данных), сравнивая
текущие значения сущности с исходными. Исходные значения сохраняются при
запросе или присоединении сущности. Ниже перечислены некоторые из методов,
которые приводят к автоматическому обнаружению изменений:
DbContext.SaveChanges
DbContext.Entry
ChangeTracker.Entries
Если вы отслеживаете большое число сущностей и многократно вызываете один из
этих методов в цикле, вы сможете добиться заметного повышения
производительности, отключив автоматическое обнаружение изменений с
помощью свойства ChangeTracker.AutoDetectChangesEnabled . Пример:
C#
_context.ChangeTracker.AutoDetectChangesEnabled = false;
EF Core исходный код и планы разработки
Источник Entity Framework Core расположен на странице
https://github.com/dotnet/efcore . Репозиторий EF Core содержит ночные сборки,
отслеживание проблем, спецификации функций, заметки о разработке собраний и
стратегию развития в будущем . Вы также можете сообщать об ошибках, находить
сведения об обнаруженных проблемах и участвовать в работе сообщества.
Несмотря на открытый исходный код, платформа Entity Framework Core полностью
поддерживается как продукт корпорации Майкрософт. Команда Microsoft Entity
Framework контролирует предложения участников, принимает их и тестирует
любые изменения кода, чтобы обеспечить максимальное качество каждого
выпуска.
Реконструирование из существующей базы
данных
Чтобы реконструировать модель данных, включая классы сущностей, из
существующей базы данных, используйте команду scaffold-dbcontext. Ознакомьтесь
с учебником по началу работы.
Использование динамических запросов
LINQ для упрощения кода
В третьем учебнике этой серии демонстрируется написание кода LINQ с жестко
запрограммированными именами столбцов в инструкции switch . При наличии
всего двух столбцов такой подход эффективен, однако если столбцов много, код
может стать слишком громоздким. Чтобы устранить эту проблему, можно
использовать метод EF.Property для указания имени свойства в виде строки. Чтобы
попробовать этот подход, замените метод Index в StudentsController следующим
кодом.
C#
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] =
String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
ViewData["DateSortParm"] =
sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" :
"EnrollmentDate";
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
if (string.IsNullOrEmpty(sortOrder))
{
sortOrder = "LastName";
}
bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
descending = true;
}
if (descending)
{
students = students.OrderByDescending(e => EF.Property<object>(e,
sortOrder));
}
else
{
students = students.OrderBy(e => EF.Property<object>(e,
sortOrder));
}
int pageSize = 3;
return View(await
PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
pageNumber ?? 1, pageSize));
}
Благодарности
Этот учебник написан Томом Дайкстра (Tom Dykstra) и Риком Андерсоном (Rick
Anderson) (twitter @RickAndMSFT)). Помощь в проверке кода для учебника и
отладке проблем, возникавших при его написании, оказывали Роуэн Миллер
(Rowan Miller), Диего Вега (Diego Vega) и другие участники команды Entity
Framework. Джон Парент (John Parente) и Пол Голдмен (Paul Goldman) обновили это
руководство для версии ASP.NET Core 2.2.
Устранение неполадок при
распространенных ошибках
Библиотека ContosoUniversity.dll используется другим
процессом
Сообщение об ошибке:
Не удается открыть файл "...bin\Debug\netcoreapp1.0\ContosoUniversity.dll" для
записи — "Процесс не может получить доступ к файлу
"...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll", так как этот файл занят
другим процессом.
Решение:
Остановите сайт в IIS Express. Найдите значок IIS Express в панели задач Windows,
щелкните его правой кнопкой мыши, выберите сайт университета Contoso и затем
выберите Остановить сайт.
Формирование шаблонов миграции без кода в
методах Up и Down
Возможная причина:
Команды интерфейса командной строки EF не выполняют автоматическое
закрытие и сохранение файлов кода. Если при выполнении команды migrations
add у вас есть несохраненные изменения, платформа EF не сможет обнаружить их.
Решение:
Выполните команду migrations remove , сохраните изменения в коде и повторно
выполните команду migrations add .
Ошибки во время обновления базы данных
При изменении схемы в базе, содержащей существующие данные, возможны
другие ошибки. Если вы получаете ошибки миграции, которые не удается
устранить, измените имя базы данных в строке подключения или удалите базу
данных. В новой базе не будет данных, которые требуется перенести, в результате
чего команда обновления базы данных с большей долей вероятности завершится
без ошибок.
Проще всего в этом случае переименовать базу данных в файле appsettings.json .
В следующий раз при выполнении команды database update будет создана новая
база данных.
Чтобы удалить базу данных в средстве SSOX, щелкните ее правой кнопкой мыши,
выберите Удалить, а затем в диалоговом окне Удаление базы данных выберите
Закрыть существующие соединения и нажмите кнопку ОК.
Чтобы удалить базу данных с помощью интерфейса командной строки, выполните
команду database drop :
Интерфейс командной строки.NET
dotnet ef database drop
Ошибка при обнаружении экземпляра SQL Server
Сообщение об ошибке:
При подключении к SQL Server произошла ошибка, связанная с сетью или с
определенным экземпляром. Сервер не найден или недоступен. Проверьте
правильность имени экземпляра и настройку сервера SQL Server для удаленных
подключений. (поставщик: сетевые интерфейсы SQL, ошибка: 26 — ошибка при
обнаружении указанного сервера или экземпляра)
Решение:
Проверьте строку подключения. Если вы вручную удалили файл базы данных,
измените имя базы данных в строке подключения, чтобы начать работу с новой
базой.
Получите код
Скачайте или ознакомьтесь с готовым приложением.
Дополнительные ресурсы
Дополнительные сведения см EF Core. в документации по Entity Framework Core.
Можно также прочесть книгу Entity Framework Core in Action .
Дополнительные сведения о развертывании веб-приложения см. в разделе
Размещение и развертывание ASP.NET Core.
Дополнительные сведения по другим вопросам, связанным с использованием
ASP.NET Core MVC, включая способы проверки подлинности и авторизации, см. в
статье Общие сведения об ASP.NET Core.
Дальнейшие действия
В этом учебнике рассмотрены следующие задачи.
" Выполнение прямых SQL-запросов
" Вызов запроса для получения сущностей
" Вызов запроса для получения других типов
" Вызов запроса на обновление
" Изучение SQL-запросов
" Создание уровня абстракции
" Сведения об автоматическом обнаружении изменений
" Сведения о исходном коде EF Core и планах разработки
" Сведения об использовании динамических запросов LINQ для упрощения кода
На этом серия учебников, посвященных использованию платформы Entity
Framework Core в приложении ASP.NET Core MVC, завершена. В этой серии мы
работали с новой базой данных; альтернативой является реконструирование базы
данных в модель.
EF Core Руководство. Использование MVC с существующей базой данных
ASP.NET Core, основы, обзор
Статья • 28.11.2022 • Чтение занимает 15 мин
В этой статье представлены основные этапы создания приложений ASP.NET Core,
включая внедрение зависимостей (DI), конфигурацию, ПО промежуточного слоя и
многое другое.
Program.cs
Приложения ASP.NET Core, созданные на основе веб-шаблонов, содержат код
запуска приложения в файле Program.cs . В файле Program.cs :
Настраиваются службы, необходимые приложению.
Конвейер обработки запросов приложения определен как ряд компонентов
ПО промежуточного слоя.
Следующий код запуска приложения поддерживает:
страницы Razor;
контроллеры MVC с представлениями;
веб-API с контроллерами;
Минимальные веб-API
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapGet("/hi", () => "Hello!");
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Введение зависимостей (службы)
ASP.NET Core включает внедрение зависимостей, позволяющее обращаться к
настроенным службам в рамках приложения. Службы добавляются в контейнер
внедрения зависимостей с помощью WebApplicationBuilder.Services,
builder.Services в предыдущем коде. При создании экземпляра
WebApplicationBuilder добавляется множество предоставляемых платформой
служб. builder — это WebApplicationBuilder в приведенном ниже коде:
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
В приведенном выше код builder добавляет в контейнер внедрения зависимостей
конфигурацию, ведение журнала и множество других служб.
Следующий код добавляет в контейнер внедрения зависимостей Razor Pages,
контроллеры MVC с представлениями и настраиваемый DbContext:
C#
using Microsoft.EntityFrameworkCore;
using RazorPagesMovie.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<RazorPagesMovieContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("RPMovieConte
xt")));
var app = builder.Build();
Как правило, службы разрешаются из системы внедрения зависимостей с
помощью внедрения конструктора. Платформа внедрения зависимостей
предоставляет экземпляр этой службы во время выполнения.
В следующем коде используется внедрение конструктора для решения контекста
базы данных и средство ведения журнала из внедрения зависимостей:
C#
public class IndexModel : PageModel
{
private readonly RazorPagesMovieContext _context;
private readonly ILogger<IndexModel> _logger;
public IndexModel(RazorPagesMovieContext context, ILogger<IndexModel>
logger)
{
_context = context;
_logger = logger;
}
public IList<Movie> Movie { get;set; }
public async Task OnGetAsync()
{
_logger.LogInformation("IndexModel OnGetAsync.");
Movie = await _context.Movie.ToListAsync();
}
}
ПО промежуточного слоя
Конвейер обработки запросов состоит из ряда компонентов ПО промежуточного
слоя. Каждый компонент выполняет операции в HttpContext, а затем либо
вызывает следующий компонент в конвейере, либо завершает запрос.
По принятому соглашению компонент ПО промежуточного слоя добавляется в
конвейер вызовом метода расширения Use{Feature} . ПО промежуточного слоя,
добавленное в приложение, выделено в следующем коде:
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapGet("/hi", () => "Hello!");
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Дополнительные сведения см. в статье ПО промежуточного слоя ASP.NET Core.
Узел
При запуске приложение ASP.NET Core создает узел. Узел инкапсулирует все
ресурсы приложения:
Реализация HTTP-сервера
Компоненты ПО промежуточного слоя
Ведение журнала
Службы внедрения зависимостей
Параметр Configuration
Существует три разных узла:
Узел веб-приложений.NET. также известный как минимальный узел.
Универсальный узел .NET
Веб-узел ASP.NET Core
Рекомендуется узел веб-приложений .NET, который используется во всех шаблонах
ASP.NET Core. Узел веб-приложений и универсальный узел .NET используют
множество одинаковых интерфейсов и классов. Веб-узел ASP.NET Core доступен
только для обеспечения обратной совместимости.
В следующем примере создается экземпляр узла веб-приложений:
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
Метод WebApplicationBuilder.Build настраивает узел с параметрами по умолчанию,
например с такими:
Используйте Kestrel в качестве веб-сервера и включите интеграцию IIS.
Загрузка конфигурации из файла appsettings.json , переменных среды,
аргументов командной строки и других источников конфигураций.
Отправка выходных данных журнала в поставщики служб консоли и отладки.
Сценарии, не связанные с Интернетом
Универсальный узел позволяет приложениям других типов использовать
перекрестные расширения платформ, такие как средства ведения журналов,
внедрения зависимостей, настройки и управления жизненным циклом.
Дополнительные сведения см. в статье Универсальный узел .NET в ASP.NET Core и
Фоновые задачи с размещенными службами в ASP.NET Core.
Серверы
Приложение ASP.NET Core использует реализацию HTTP-сервера для приема HTTPзапросов. Сервер отправляет приложению запросы в виде набора функций
запросов, объединенных в HttpContext .
Windows
ASP.NET Core предоставляет следующие реализации серверов:
Kestrel представляет собой кроссплатформенный веб-сервер. Kestrel
зачастую запускается в конфигурации обратного прокси с
использованием службы IIS . В ASP.NET Core 2.0 или более поздних
версиях Kestrel может быть запущен как общедоступный пограничный
сервер, напрямую подключенный к Интернету.
HTTP-сервер IIS — это сервер для Windows, который использует
службы IIS. Он позволяет запускать приложение ASP.NET Core и
службы IIS в одном процессе.
HTTP.sys — это сервер для Windows, который не используется со
службами IIS.
Дополнительные сведения см. в статье Реализация веб-сервера в ASP.NET Core.
Конфигурация
ASP.NET Core предоставляет платформу конфигурации, которая получает
параметры в виде пар "имя-значение" от упорядоченного набора поставщиков
конфигурации. Доступны встроенные поставщики конфигурации для различных
источников, таких как файлы .json , .xml , переменные среды и аргументы
командной строки. Для поддержки других источников можно создать
настраиваемые поставщики конфигурации.
По умолчанию приложения ASP.NET Core настроены для чтения из файла
appsettings.json , переменных среды, командной строки и т. д. При загрузке
конфигурации приложения значения из переменных среды переопределяют
значения из файла appsettings.json .
Для управления конфиденциальными данными конфигурации, например
паролями, .NET Core предоставляет диспетчер секретов. Для секретов в рабочей
среде рекомендуется использовать Azure Key Vault.
Дополнительные сведения см. в разделе Конфигурация в ASP.NET Core.
Среды
Среды выполнения, такие как Development , Staging и Production , доступны в
ASP.NET Core. Указать среду, в которой запускается приложение, можно с
помощью переменной среды ASPNETCORE_ENVIRONMENT . ASP.NET Core считывает
переменную среды при запуске приложения и сохраняет ее значение в
реализации IWebHostEnvironment . Эта реализация доступна в любом месте
приложения посредством внедрения зависимостей.
В следующем примере выполняется настройка обработчика исключений и ПО
промежуточного слоя протокола HTTP Strict Transport Security Protocol (HSTS) при
выполнении НЕ в среде Development :
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapGet("/hi", () => "Hello!");
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Дополнительные сведения см. в статье Использование нескольких сред в ASP.NET
Core.
Ведение журнала
ASP.NET Core поддерживает API ведения журналов, который работает с разными
встроенными и сторонними поставщиками. Доступные следующие поставщики:
Консоль
Отладка
Трассировка событий Windows
Журнал событий Windows
TraceSource
Служба приложений Azure
Azure Application Insights
Для создания журналов необходимо разрешить службу ILogger<TCategoryName>
из системы внедрения зависимостей (DI) и вызвать методы ведения журналов,
такие как LogInformation. Пример:
C#
public class IndexModel : PageModel
{
private readonly RazorPagesMovieContext _context;
private readonly ILogger<IndexModel> _logger;
public IndexModel(RazorPagesMovieContext context, ILogger<IndexModel>
logger)
{
_context = context;
_logger = logger;
}
public IList<Movie> Movie { get;set; }
public async Task OnGetAsync()
{
_logger.LogInformation("IndexModel OnGetAsync.");
Movie = await _context.Movie.ToListAsync();
}
}
Дополнительные сведения см. в разделе Ведение журнала в .NET Core и ASP.NET
Core.
Маршрутизация
Маршрут — это шаблон URL-адреса, сопоставляемый с обработчиком. Обычно
обработчик представляет собой страницу Razor, метод действия в контроллере
MVC или ПО промежуточного слоя. Маршрутизация ASP.NET Core позволяет
контролировать URL-адреса, используемые приложением.
Следующий код, созданный шаблоном веб-приложения ASP.NET Core, вызывает
UseRouting:
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Дополнительные сведения см. в статье Маршрутизация в ASP.NET Core.
Обработка ошибок
ASP.NET Core содержит встроенные функции обработки ошибок, такие как:
Страница исключений для разработчика
Настраиваемые страницы ошибок
Статические страницы с кодами состояния
Обработка исключений при запуске
Дополнительные сведения см. в статье Обработка ошибок в ASP.NET Core.
Создание HTTP-запросов
ASP.NET Core включает реализацию интерфейса IHttpClientFactory для создания
экземпляров HttpClient . Фабрика:
Центральное расположение для именования и настройки логических
экземпляров HttpClient . Например, можно зарегистрировать и использовать
клиент github для доступа к сайту GitHub. Можно зарегистрировать и
настроить клиент по умолчанию для других целей.
Поддержка регистрации и связывания в цепочки множества делегирующих
обработчиков для создания конвейера ПО промежуточного слоя под
исходящие запросы. Этот шаблон похож на входящий конвейер ПО
промежуточного слоя для ASP.NET Core. Шаблон предоставляет механизм
управления сквозной функциональностью HTTP-запросов, включая
кэширование, обработку ошибок, сериализацию и ведение журнала.
Интеграция с Polly — популярной сторонней библиотекой для обработки
временных сбоев.
Управление созданием пулов и временем существования базовых
экземпляров HttpClientHandler с целью избежать обычных проблем с DNS,
которые возникают при управлении временем существования HttpClient
вручную.
Настройка параметров ведения журнала (через ILogger) для всех запросов,
отправленных через клиентов, созданных фабрикой.
Дополнительные сведения см. в статье Выполнение HTTP-запросов с помощью
IHttpClientFactory в ASP.NET Core.
Корневой каталог содержимого
Корневой каталог содержимого — это базовый путь к следующим элементам:
Исполняемый файл, в котором размещено приложение (EXE).
Скомпилированные сборки, составляющие приложение (DLL).
Файлы содержимого, используемые приложением, например:
Файлы Razor ( .cshtml , .razor )
Файлы конфигурации ( .json , .xml )
Файлы данных ( .db )
Корневой веб-каталог, обычно это папка wwwroot.
Во время развертывания корень содержимого по умолчанию сбрасывается до
корневого каталога проекта. Этот каталог является базовым путем к файлам
содержимого приложения и корневому веб-каталогу. Альтернативный корневой
путь к содержимому может быть указан при создании узла. Дополнительные
сведения: Корень содержимого.
Корневой веб-узел
Корневой веб-каталог — это базовый путь к общедоступным файлам статических
ресурсов, например:
Таблицы стилей ( .css )
JavaScript ( .js )
Изображения ( .png , .jpg )
По умолчанию статические файлы обслуживаются только в корневом веб-каталоге
и его подкаталогах. По умолчанию используется путь {корневой каталог
содержимого}/wwwroot. Альтернативное расположение корневого веб-каталога
можно указать при создании узла. Дополнительные сведения см. в разделе
Корневой веб-каталог.
Запретите публикацию файлов в wwwroot с помощью элемента проекта <Content>
в файле проекта. В следующем примере запрещается публикация содержимого в
каталоге wwwroot/local и его подкаталогах:
XML
<ItemGroup>
<Content Update="wwwroot\local\**\*.*" CopyToPublishDirectory="Never" />
</ItemGroup>
В файлах Razor .cshtml ~/ указывает на корневой каталог. Путь, начинающийся с
~/ , называется виртуальным путем.
Подробные сведения см. в статье Статические файлы в ASP.NET Core.
Дополнительные ресурсы
Исходный код WebApplicationBuilder
Запуск приложения в ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 7 мин
Автор: Рик Андерсон
(Rick Anderson)
Приложения ASP.NET Core, созданные на основе веб-шаблонов, содержат код
запуска приложения в файле Program.cs .
Следующий код запуска приложения поддерживает:
страницы Razor;
контроллеры MVC с представлениями;
веб-API с контроллерами;
Минимальные API
C#
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthorization();
app.MapGet("/hi", () => "Hello!");
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
Приложения, использующие EventSource, могут измерять время запуска, чтобы
определить и оптимизировать его производительность. Событие ServerReady
в
Microsoft.AspNetCore.Hosting представляет точку, в которой сервер готов отвечать
на запросы.
Дополнительные сведения о запуске приложения см. в статье Основы ASP.NET
Core.
Внедрение зависимостей в ASP.NET
Core
Статья • 28.01.2023 • Чтение занимает 28 мин
Авторы: Кирк Ларкин
(Kirk Larkin), Стив Смит
(Steve Smith) и Брэндон Далер
(Brandon Dahler)
ASP.NET Core поддерживает проектирование программного обеспечения с
возможностью внедрения зависимостей. При таком подходе достигается инверсия
управления между классами и их зависимостями.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в
статье Внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в
приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.
Дополнительные сведения о внедрении параметров зависимостей см. в разделе
Шаблон параметров в ASP.NET Core.
В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core.
Основная документация по использованию внедрения зависимостей указана в
статье Внедрение зависимостей в .NET.
Просмотреть или скачать образец кода
(как скачивать)
Общие сведения о внедрении зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим
следующий класс MyDependency с методом WriteMessage , от которого зависят другие
классы:
C#
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message:
{message}");
}
}
Класс может создать экземпляр класса MyDependency , чтобы использовать его метод
WriteMessage . В следующем примере класс MyDependency выступает зависимостью
класса IndexModel :
C#
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
Этот класс создает MyDependency и напрямую зависит от этого класса. Зависимости в
коде, как в предыдущем примере, представляют собой определенную проблему.
Их следует избегать по следующим причинам.
Чтобы заменить MyDependency другой реализацией, класс IndexModel
необходимо изменить.
Если у MyDependency есть зависимости, их конфигурацию должен выполнять
класс IndexModel . В больших проектах, когда от MyDependency зависят многие
классы, код конфигурации растягивается по всему приложению.
Такая реализация плохо подходит для модульных тестов.
Внедрение зависимостей устраняет эти проблемы следующим образом:
Используется интерфейс или базовый класс для абстрагирования реализации
зависимостей.
Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет
встроенный контейнер служб, IServiceProvider. Как правило, службы
регистрируются в файле Program.cs приложения.
Служба внедряется в конструктор класса там, где он используется. Платформа
берет на себя создание экземпляра зависимости и его удаление, когда он
больше не нужен.
В примере приложения
интерфейс IMyDependency определяет метод WriteMessage :
C#
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency .
C#
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency с конкретным типом
MyDependency . Метод AddScoped регистрирует службу с заданной областью
времени существования, временем существования одного запроса. Подробнее о
времени существования служб мы поговорим далее в этой статье.
C#
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
В примере приложения запрашивается служба IMyDependency , которая затем
используется для вызова метода WriteMessage :
C#
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя шаблон внедрения зависимостей, контроллер или страницу Razor:
не использует конкретный тип MyDependency , только интерфейс IMyDependency ,
который он реализует. Это упрощает изменение реализации без изменения
контроллера или страницы Razor.
не создает экземпляр MyDependency , он создается контейнером внедрения
зависимостей.
Реализацию интерфейса IMyDependency можно улучшить с помощью встроенного
API ведения журнала:
C#
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message:
{message}");
}
}
Обновленный метод Program.cs регистрирует новую реализацию IMyDependency :
C#
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
var app = builder.Build();
MyDependency2 зависит от ILogger<TCategoryName>, который запрашивается в
конструкторе. ILogger<TCategoryName> — это предоставленная платформой служба.
Использование цепочки внедрений зависимостей не является чем-то необычным.
Каждая запрашиваемая зависимость запрашивает собственные зависимости.
Контейнер разрешает зависимости в графе и возвращает полностью разрешенную
службу. Весь набор зависимостей, которые нужно разрешить, обычно называют
деревом зависимостей, графом зависимостей или графом объектов.
Контейнер разрешает ILogger<TCategoryName> , используя преимущества
(универсальных) открытых типов, что устраняет необходимость регистрации
каждого (универсального) сконструированного типа.
В терминологии внедрения зависимостей — служба:
Обычно является объектом, предоставляющим службу для других объектов,
например службу IMyDependency .
Не относится к веб-службе, хотя служба может использовать веб-службу.
Платформа предоставляет эффективную систему ведения журнала. Реализации
IMyDependency , приведенные в предыдущем примере были написаны для
демонстрации базового внедрения зависимостей, а не для реализации ведения
журнала. Большинству приложений не нужно писать средства ведения журнала. В
следующем коде показано использование журнала по умолчанию, для которого не
требуется регистрация служб:
C#
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; } = string.Empty;
public void OnGet()
{
Message = $"About page visited at
{DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
Используя приведенный выше код, не нужно обновлять Program.cs , поскольку
платформа предоставляет возможность ведения журнала.
Регистрация групп служб с помощью
методов расширения
Для регистрации группы связанных служб на платформе ASP.NET Core используется
соглашение. Соглашение заключается в использовании одного метода расширения
Add{GROUP_NAME} для регистрации всех служб, необходимых компоненту платформы.
Например, метод расширения AddControllers регистрирует службы, необходимые
контроллерам MVC.
Следующий код создается шаблоном Razor Pages с использованием отдельных
учетных записей пользователей. Он демонстрирует, как добавить дополнительные
службы в контейнер с помощью методов расширения AddDbContext и
AddDefaultIdentity:
C#
using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
Рассмотрим следующий код, который регистрирует службы и настраивает
параметры:
C#
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();
Связанные группы регистраций можно переместить в метод расширения для
регистрации служб. Например, службы конфигурации добавляются в следующий
класс:
C#
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
Остальные службы регистрируются в аналогичном классе. Следующий код
использует новые методы расширения для регистрации служб:
C#
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
Примечание. Каждый метод расширения services.Add{GROUP_NAME} добавляет и,
возможно, настраивает службы. Например, AddControllersWithViews добавляет
контроллеры MVC служб с необходимыми представлениями, а AddRazorPages —
службы, требуемые для работы Razor Pages.
Время существования служб
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.
Используйте службы с заданной областью в ПО промежуточного слоя, применяя
один из следующих подходов:
Внедрите службу в метод Invoke или InvokeAsync ПО промежуточного слоя. С
помощью внедрите конструктор создается исключение времени выполнения,
поскольку оно заставляет службу с заданной областью вести себя как
одноэлементный объект. В примере в разделе Параметры времени
существования и регистрации демонстрируется подход InvokeAsync .
Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя,
зарегистрированное с использованием этого подхода, активируется при
каждом клиентском запросе (подключении), что позволяет внедрять службы с
заданной областью в конструктор ПО промежуточного слоя.
Дополнительные сведения см. в разделе Создание пользовательского ПО
промежуточного слоя ASP.NET Core.
Методы регистрации службы
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.
Распространенный сценарий для использования нескольких реализаций —
макетирование типов для тестирования.
Регистрация службы только с типом реализации эквивалентна регистрации этой
службы с той же реализацией и типом службы. Именно поэтому несколько
реализаций службы не могут быть зарегистрированы с помощью методов, которые
не принимают явный тип службы. Эти методы могут регистрировать несколько
экземпляров службы, но все они будут иметь одинаковую реализацию типа.
Любой из указанных выше методов регистрации службы можно использовать для
регистрации нескольких экземпляров службы одного типа службы. В следующем
примере метод AddSingleton вызывается дважды с типом службы IMyDependency .
Второй вызов AddSingleton переопределяет предыдущий, если он разрешается как
IMyDependency , и добавляет к предыдущему, если несколько служб разрешаются
через IEnumerable<IMyDependency> . Службы отображаются в том порядке, в котором
они были зарегистрированы при разрешении через IEnumerable<{SERVICE}> .
C#
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
Поведение внедрения через конструктор
См. раздел Поведение при внедрении конструктора в статье Внедрение
зависимостей в .NET.
Контексты Entity Framework
По умолчанию контексты Entity Framework добавляются в контейнер службы с
помощью времени существования с заданной областью, поскольку операции базы
данных в веб-приложении обычно относятся к области клиентского запроса. Чтобы
использовать другое время существования, укажите его с помощью перегрузки
AddDbContext. Службы данного времени существования не должны использовать
контекст базы данных с временем существования короче, чем у службы.
Параметры времени существования и
регистрации
Чтобы продемонстрировать различия между указанными вариантами времени
существования и регистрации службы, рассмотрим интерфейсы, представляющие
задачу в виде операции с идентификатором OperationId . В зависимости от того, как
время существования службы операции настроено для этих интерфейсов, при
запросе из класса контейнер предоставляет тот же или другой экземпляр службы.
C#
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation реализует все предыдущие интерфейсы. Конструктор
Operation создает идентификатор GUID и сохраняет последние 4 символа в
свойстве OperationId :
C#
public class Operation : IOperationTransient, IOperationScoped,
IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Следующий код создает несколько регистраций класса Operation в соответствии с
именованным временем существования:
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMyMiddleware();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
В примере приложения показано время существования объектов в пределах
запросов и между запросами. IndexModel и ПО промежуточного слоя запрашивают
каждый тип IOperation и регистрируют OperationId для каждого из них:
C#
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation
= scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " +
_transientOperation.OperationId);
_logger.LogInformation("Scoped: "
+
_scopedOperation.OperationId);
_logger.LogInformation("Singleton: " +
_singletonOperation.OperationId);
}
}
Аналогично IndexModel , ПО промежуточного слоя и разрешает те же службы:
C#
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped
scopedOperation)
{
_logger.LogInformation("Transient: " +
transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " +
_singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this
IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
Службы с заданной областью и временные службы должны быть разрешены в
методе InvokeAsync :
C#
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped
scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
Выходные данные средства ведения журнала содержат:
Временные объекты всегда разные. Значение временного OperationId
отличается в IndexModel и ПО промежуточного слоя.
Объекты с заданной областью остаются неизменными в пределах указанного
запроса, но в новых запросах используются разные объекты.
Одноэлементные объекты одинаковы для каждого запроса.
Чтобы уменьшить объем выводимых данных журнала, задайте в файле
appsettings.Development.json параметр "Logging:LogLevel:Microsoft:Error".
JSON
{
"MyKey": "MyKey from appsettings.Developement.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Разрешение службы при запуске
приложения
В следующем коде показано, как разрешить службу с областью действия в течение
ограниченного времени при запуске приложения:
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
app.MapGet("/", () => "Hello World!");
app.Run();
Проверка области
См. раздел Поведение при внедрении конструктора в статье Внедрение
зависимостей в .NET.
Дополнительные сведения см. в разделе Проверка области.
Службы запросов
Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью
свойства HttpContext.RequestServices.
Платформа создает область для каждого запроса, а RequestServices предоставляет
поставщик услуг с заданной областью. Все службы с заданной областью
действительны до тех пор, пока запрос активен.
7 Примечание
Предпочтительнее запрашивать зависимости в качестве параметров
конструктора, а не разрешать службы из RequestServices . Таким образом вы
получите классы, которые проще тестировать.
Проектирование служб для внедрения
зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих
рекомендаций:
Избегайте статических классов и членов с отслеживанием состояния.
Избегайте создания глобального состояния. Для этого проектируйте
приложения для использования отдельных служб.
Избегайте прямого создания экземпляров зависимых классов внутри служб.
Прямое создание экземпляров обязывает использовать в коде определенную
реализацию.
Сделайте службы приложения небольшими, хорошо организованными и
удобными в тестировании.
Если класс имеет слишком много внедренных зависимостей, это может указывать
на то, что у класса слишком много задач и он нарушает принцип единственной
обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его
обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages
и классах контроллера MVC должны преимущественно выполняться задачи,
связанные с пользовательским интерфейсом.
Удаление служб
Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы,
разрешенные из контейнера, никогда не должны удаляться разработчиком. Если
тип или фабрика зарегистрированы как одноэлементный объект, контейнер
автоматически удалит одноэлементные объекты.
В следующем примере службы создаются контейнером службы и автоматически
удаляются: dependency-injection\samples\6.x\DIsample2\Services\Service1.cs
C#
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
C#
using DIsample2.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));
var app = builder.Build();
C#
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3
service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются
следующие выходные данные:
Консоль
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet
Service1.Dispose
Службы, не созданные контейнером службы
Рассмотрим следующий код.
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
В приведенном выше коде:
Экземпляры службы не создаются контейнером службы.
Платформа не удаляет службы автоматически.
За удаление служб отвечает разработчик.
Руководство по применению временных и общих
экземпляров IDisposable
См. раздел Рекомендации по IDisposable при использовании промежуточного и
общего экземпляра в статье Внедрение зависимостей в .NET.
Замена стандартного контейнера служб
См. раздел Замена контейнера службы по умолчанию в статье Внедрение
зависимостей в .NET.
Рекомендации
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.
Старайтесь не использовать схему указателя служб. Например, не вызывайте
GetService для получения экземпляра службы, когда можно использовать
внедрение зависимостей:
Неправильно:
Правильно:
C#
public class MyClass
{
private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
}
public void MyMethod()
{
var option = _optionsMonitor.CurrentValue.Option;
...
}
}
Другой вариант указателя службы, позволяющий избежать этого, —
внедрение фабрики, которая разрешает зависимости во время выполнения.
Оба метода смешивают стратегии инверсии управления.
Не используйте статический доступ к HttpContext (например,
IHttpContextAccessor.HttpContext).
Внедрение зависимостей является альтернативой для шаблонов доступа к
статическим или глобальным объектам. Вы не сможете воспользоваться
преимуществами внедрения зависимостей, если будете сочетать его с доступом к
статическим объектам.
Рекомендуемые подходы к
мультитенантности при внедрении
зависимостей
Orchard Core
— это платформа приложений для создания модульных
мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в
документации по Orchard Core .
Примеры создания модульных и мультитенантных приложений с использованием
только Orchard Core Framework без каких-либо особых функций CMS см. здесь .
Платформенные службы
Program.cs регистрирует службы, которые использует приложение, включая такие
компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция
IServiceCollection , предоставленная для Program.cs , содержит определенные
платформой службы (в зависимости от настройки узла). Для приложений,
основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.
В следующей таблице перечислены некоторые примеры этих зарегистрированных
платформой служб.
Тип службы
Время существования
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory
Временный
IHostApplicationLifetime
Одноэлементный
IWebHostEnvironment
Одноэлементный
Microsoft.AspNetCore.Hosting.IStartup
Одноэлементный
Microsoft.AspNetCore.Hosting.IStartupFilter
Временный
Microsoft.AspNetCore.Hosting.Server.IServer
Одноэлементный
Microsoft.AspNetCore.Http.IHttpContextFactory
Временный
Тип службы
Время существования
Microsoft.Extensions.Logging.ILogger<TCategoryName>
Одноэлементный
Microsoft.Extensions.Logging.ILoggerFactory
Одноэлементный
Microsoft.Extensions.ObjectPool.ObjectPoolProvider
Одноэлементный
Microsoft.Extensions.Options.IConfigureOptions<TOptions>
Временный
Microsoft.Extensions.Options.IOptions<TOptions>
Одноэлементный
System.Diagnostics.DiagnosticSource
Одноэлементный
System.Diagnostics.DiagnosticListener
Одноэлементный
Дополнительные ресурсы
Внедрение зависимостей в представления в ASP.NET Core
Внедрение зависимостей в контроллеры в ASP.NET Core
Внедрение зависимостей в обработчики требований в ASP.NET Core
Внедрение зависимостей Blazor в ASP.NET Core
Шаблоны конференций NDC для разработки приложений с внедрением
зависимостей
Запуск приложения в ASP.NET Core
Активация ПО промежуточного слоя на основе фабрики в ASP.NET Core
Четыре способа удаления интерфейсов IDisposable в ASP.NET Core
Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)
Принцип явных зависимостей
Контейнеры с инверсией управления и шаблон внедрения зависимостей
(Мартин Фаулер)
How to register a service with multiple interfaces in ASP.NET Core DI
(Регистрация службы с несколькими интерфейсами с помощью внедрения
зависимостей ASP.NET Core)
ПО промежуточного слоя ASP.NET
Core
Статья • 28.01.2023 • Чтение занимает 41 мин
Авторы: Рик Андерсон
(Rick Anderson) и Стив Смит
(Steve Smith)
ПО промежуточного слоя — это программное обеспечение, выстраиваемое в виде
конвейера приложения для обработки запросов и откликов. Каждый компонент:
определяет, нужно ли передать запрос следующему компоненту в конвейере;
может выполнять работу как до, так и после вызова следующего компонента
в конвейере.
Для построения конвейера запросов используются делегаты запроса. Они
обрабатывают каждый HTTP-запрос.
Для их настройки служат методы расширения Run, Map и Use. Отдельный делегат
запроса можно указать встроенным в качестве анонимного метода (называемого
встроенным ПО промежуточного слоя) либо определить в многоразовом классе.
Эти многоразовые классы и встроенные анонимные методы являются ПО
промежуточного слоя или компонентами промежуточного слоя. Каждый
компонент ПО промежуточного слоя в конвейере запросов отвечает за вызов
следующего компонента в конвейере или замыкает конвейер. Когда
промежуточный слой замыкает конвейер, он становится терминальным
промежуточным слоем, так как препятствует обработке запроса дальнейшими
компонентами промежуточного слоя.
В статье Перенос обработчиков HTTP-данных и модулей HTTP в ПО
промежуточного слоя ASP.NET Core поясняются различия между конвейерами
запросов в ASP.NET Core и ASP.NET 4.x, а также приводятся дополнительные
примеры ПО промежуточного слоя.
Анализ кода ПО промежуточного слоя
ASP.NET Core содержит множество анализаторов .NET Compiler Platform,
проверяющих качество кода приложения. Дополнительные сведения см. в статье
Анализ кода в приложениях ASP.NET Core.
Создание конвейера ПО промежуточного
слоя с помощью WebApplication
Конвейер запросов ASP.NET Core состоит из последовательности делегатов
запроса, вызываемых один за другим. На следующей схеме демонстрируется этот
принцип. Поток выполнения показан черными стрелками.
Каждый из делегатов может выполнять операции до и после следующего делегата.
Делегаты обработки исключений должны вызываться в начале конвейера, чтобы
перехватывать исключения, возникающие на более поздних этапах.
Простейшее приложение ASP.NET Core задает один делегат запроса,
обрабатывающий все запросы. В этом случае конвейер запросов как таковой
отсутствует. Вместо этого в ответ на каждый HTTP-запрос вызывается одна
анонимная функция.
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello world!");
});
app.Run();
Несколько делегатов запроса можно соединить в цепочку с помощью Use.
Параметр next представляет следующий делегат в конвейере. Замыкать конвейер
можноне вызывая параметр next . Обычно действия можно выполнять как до, так и
после next делегата, как показано в этом примере:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
app.Run();
Если делегат не передает запрос следующему делегату, это называется замыканием
конвейера запросов. Замыкание часто является предпочтительным, так как
позволяет избежать ненужной работы. Например, компонент промежуточного
слоя для статических файлов может выступать в роли терминального
промежуточного слоя, обрабатывая запрос статического файла и замыкая
оставшуюся часть конвейера. Компоненты промежуточного слоя, предшествующие
терминальному промежуточному слою, по-прежнему обрабатывают код после их
инструкций next.Invoke . Но учитывайте следующее предупреждение о попытке
записи в ответ, который уже был отправлен.
2 Предупреждение
Не вызывайте next.Invoke после отправки отклика клиенту. Изменения
HttpResponse после запуска отклика приведут к возникновению исключения.
Таким изменением может быть задание заголовков и кода состояния. Запись
в тело отклика после вызова next :
может вызвать нарушение протокола, например, при записи больше
указанного значения Content-Length ;
может привести к нарушению формата, например, при записи нижнего
колонтитула HTML в CSS-файл.
HasStarted удобно использовать для обозначения того, были ли отправлены
заголовки или выполнена запись в тело отклика.
Делегаты Run не получают параметр next . Первый делегат Run всегда является
конечным и завершает конвейер. Run является соглашение. Некоторые
компоненты промежуточного слоя могут предоставлять методы Run[Middleware] ,
которые выполняются в конце конвейера:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Do work that can write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
app.Run();
Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от
английского, сообщите нам на странице обсуждения этой проблемы на сайте
GitHub .
В предыдущем примере делегат Run записывает "Hello from 2nd delegate." в ответ
и завершает конвейер. Если добавить другой делегат Use или Run после делегата
Run , он не будет вызван.
Рекомендуется перегрузка app.Use, требующая
передачи контекста далее
Метод расширения app.Use без выделения:
Требует передачи контекста в next .
Сохраняет два внутренних выделения на один запрос, которые требуются при
другой перегрузке.
Дополнительные сведения см. в этой статье об ошибке на GitHub .
Порядок ПО промежуточного слоя
На следующей схеме показан полный конвейер обработки запросов для
приложений ASP.NET Core MVC и Razor Pages. Здесь демонстрируется порядок
размещения ПО промежуточного слоя и этапы, на которых добавляется
пользовательское ПО промежуточного слоя, в стандартном приложении. Вы
можете полностью контролировать изменение порядка существующего ПО
промежуточного слоя или внедрять новое пользовательское ПО промежуточного
слоя для своих сценариев.
ПО промежуточного слоя конечной точки на предыдущей схеме выполняет
конвейер фильтра для соответствующего типа приложения — MVC или Razor Pages.
ПО промежуточного слоя маршрутизации на предыдущей схеме размещено после
статических файлов. В таком порядке реализуются шаблоны проектов путем
явного вызова app.UseRouting. Если оператор app.UseRouting не вызван, по
умолчанию ПО промежуточного слоя маршрутизации запускается в начале
конвейера. Дополнительные сведения см. в разделе Маршрутизация.
Порядок, в котором компоненты промежуточного слоя добавляются в файл
Program.cs , определяет порядок их вызова при запросах и обратный порядок для
откликов. Соблюдать этот порядок крайне важно для обеспечения безопасности,
производительности и функциональности.
Следующий выделенный код в Program.cs добавляет связанные с безопасностью
компоненты промежуточного слоя в стандартном рекомендуемом порядке:
C#
using IndividualAccountsExample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for
production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
В приведенном выше коде:
ПО промежуточного слоя, которое не было добавлено при создании вебприложения с учетными записями отдельных пользователей, деактивируется.
Этот порядок соблюдается не для любого ПО промежуточного слоя. Пример.
UseCors , UseAuthentication и UseAuthorization должны присутствовать в
указанном порядке.
Сейчас UseCors нужно использовать перед UseResponseCaching . Это
требование объясняется в вопросе GitHub по адресу dotnet/aspnetcore
#23218
.
UseRequestLocalization нужно использовать перед любым ПО
промежуточного слоя, которое может проверять язык и региональные
параметры запроса (например, app.UseMvcWithDefaultRoute() ).
В некоторых сценариях ПО промежуточного слоя имеет другой порядок.
Например, порядок кэширования и сжатия зависит от сценария, и существует
несколько допустимых вариантов такого порядка. Вот несколько примеров.
C#
app.UseResponseCaching();
app.UseResponseCompression();
С помощью приведенного выше кода можно снизить загрузку ЦП путем
кэширования сжатого ответа, но при этом может быть выполнено кэширование
нескольких представлений ресурса с помощью разных алгоритмов сжатия, таких
как Gzip или Brotli.
В приведенном ниже порядке объединяются статические файлы, чтобы разрешить
кэширование сжатых статических файлов.
C#
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
Следующий код Program.cs добавляет компоненты промежуточного слоя для
распространенных сценариев приложений:
1. Обработка исключений/ошибок
Когда приложение выполняется в среде разработки:
ПО промежуточного слоя страницы исключений для разработчика
(UseDeveloperExceptionPage) сообщает об ошибках среды выполнения
приложения.
ПО промежуточного слоя страницы исключений для базы данных
(UseDatabaseErrorPage) сообщает об ошибках среды выполнения базы
данных.
Когда приложение выполняется в рабочей среде:
ПО промежуточного слоя обработчика исключений
(UseExceptionHandler) перехватывает исключения, возникшие в
указанном ниже ПО промежуточного слоя.
ПО промежуточного слоя протокола HTTP Strict Transport Security
Protocol (HSTS) (UseHsts) добавляет заголовок Strict-TransportSecurity .
2. ПО промежуточного слоя перенаправления HTTPS (UseHttpsRedirection)
перенаправляет запросы с HTTP на HTTPS.
3. ПО промежуточного слоя статических файлов (UseStaticFiles) возвращает
статические файлы и сокращает дальнейшую обработку запросов.
4. ПО промежуточного слоя политики файлов Cookie (UseCookiePolicy)
обеспечивает соответствие приложения нормам Общего регламента по
защите данных (GDPR) ЕС.
5. ПО промежуточного слоя маршрутизации (UseRouting) для маршрутизации
запросов.
6. ПО промежуточного слоя проверки подлинности (UseAuthentication) пытается
проверить подлинность пользователя, прежде чем предоставить ему доступ к
защищенным ресурсам.
7. ПО промежуточного слоя авторизации (UseAuthorization) разрешает
пользователю доступ к защищенным ресурсам.
8. ПО промежуточного слоя сеанса (UseSession) устанавливает и поддерживает
состояние сеанса. Если в приложении используется состояние сеанса,
вызовите ПО промежуточного слоя сеанса после ПО промежуточного слоя
политики файлов Cookie и до ПО промежуточного слоя MVC.
9. ПО промежуточного слоя маршрутизации конечных точек (UseEndpoints с
MapRazorPages) для добавления конечных точек Razor Pages в конвейер
запросов.
C#
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
В предыдущем примере кода каждый метод расширения ПО промежуточного слоя
представляется в WebApplicationBuilder с использованием пространства имен
Microsoft.AspNetCore.Builder.
UseExceptionHandler — это первый компонент промежуточного слоя, добавленный
в конвейер. Таким образом, обработчик исключений ПО промежуточного слоя
перехватывает все исключения, возникающие в последующих вызовах.
Компонент промежуточного слоя для статических файлов вызывается на раннем
этапе конвейера, чтобы он мог обработать запросы и выполнить замыкание, минуя
остальные компоненты. Этот компонент не выполняет проверки авторизации. Все
обрабатываемые им файлы, включая расположенные в wwwroot, находятся в
открытом доступе. Сведения о защите статических файлов см. в статье Статические
файлы в ASP.NET Core.
Если запрос не обрабатывается компонентом промежуточного слоя для
статических файлов, он передается в компонент промежуточного слоя для
проверки подлинности (UseAuthentication), который выполняет проверку
подлинности. Этот компонент не замыкает запросы, не прошедшие проверку
подлинности. Хотя ПО промежуточного слоя для проверки подлинности проверяет
подлинность запросов, авторизация (и отклонение) выполняются только после
того, как MVC выберет указанную страницу Razor Pages или контроллер MVC и
действие.
Следующий пример показывает порядок компонентов промежуточного слоя, где
запросы для статических файлов обрабатываются компонентом для статических
файлов до компонента для сжатия откликов. Статические файлы не сжимаются с
этим порядком ПО промежуточного слоя. Ответы Razor Pages могут быть сжаты.
C#
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
Дополнительные сведения об одностраничных приложениях см. в руководствах по
шаблонам проектов React и Angular.
Порядок UseCors и UseStaticFiles
Порядок вызовов UseCors и UseStaticFiles зависит от приложения.
Дополнительные сведения см. в разделе Порядок UseCors и UseStaticFiles.
Порядок ПО промежуточного слоя перенаправления
заголовков
ПО промежуточного слоя перенаправления заголовков должно выполняться до
другого ПО промежуточного слоя. Такой порядок гарантирует, что ПО
промежуточного слоя, полагающееся на сведения о перенаправленных заголовках,
может использовать значения заголовков для обработки. Сведения о запуске ПО
промежуточного слоя перенаправления заголовков после ПО промежуточного
слоя диагностики и обработки ошибок см. в разделе Порядок ПО промежуточного
слоя перенаправления заголовков.
Ветвление конвейера ПО промежуточного
слоя
Расширения Map используются в качестве соглашения для ветвления конвейера.
Map осуществляет ветвление конвейера запросов на основе совпадений для
заданного пути запроса. Если путь запроса начинается с заданного пути, данная
ветвь выполняется.
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
В таблице ниже приведены запросы и отклики http://localhost:1234 на базе
предыдущего кода.
Запрос
Ответ
localhost:1234
Hello from non-Map delegate.
localhost:1234/map1
Map Test 1
localhost:1234/map2
Map Test 2
localhost:1234/map3
Hello from non-Map delegate.
Когда используется Map , соответствующие сегменты путей удаляются из
HttpRequest.Path и добавляются к HttpRequest.PathBase для каждого запроса.
Map поддерживает вложение, например:
C#
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
Map также может сопоставить несколько сегментов одновременно:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1/seg1", HandleMultiSeg);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
MapWhen осуществляет ветвление конвейера запросов на основе результата
заданного предиката. Любой предикат типа Func<HttpContext, bool> можно
использовать для сопоставления запросов с новой ветвью конвейера. В
следующем примере предикат служит для определения наличия переменной
строки запроса branch .
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
Ниже приведены запросы и отклики http://localhost:1234 на базе предыдущего
кода.
Запрос
Ответ
localhost:1234
Hello from non-Map delegate.
Запрос
Ответ
localhost:1234/?branch=main
Branch used = main
UseWhen также осуществляет ветвление конвейера запросов на основе результата
заданного предиката. В отличие от MapWhen , эта ветвь снова объединяется с
основным конвейером, если она не выполняется по сокращенной схеме или не
содержит конечное ПО промежуточного слоя:
C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
appBuilder => HandleBranchAndRejoin(appBuilder));
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
app.Run();
void HandleBranchAndRejoin(IApplicationBuilder app)
{
var logger =
app.ApplicationServices.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var branchVer = context.Request.Query["branch"];
logger.LogInformation("Branch used = {branchVer}", branchVer);
// Do work that doesn't write to the Response.
await next();
// Do other work that doesn't write to the Response.
});
}
В предыдущем примере ответ Hello from non-Map delegate. записывается для всех
запросов. Если запрос включает переменную строки запроса branch , ее значение
регистрируется до того, как будет выполнено повторное объединение с основным
конвейером.
Встроенное ПО промежуточного слоя
ASP.NET Core содержит следующие компоненты промежуточного слоя. В столбце
Порядок указаны сведения о размещении ПО промежуточного слоя в конвейере
обработки запросов и условия, в соответствии с которыми ПО промежуточного
слоя может прервать обработку запроса. Если промежуточный слой замыкает
конвейер обработки запроса и препятствует обработке запроса дальнейшими
компонентами промежуточного слоя, он называется терминальным
промежуточным слоем. Дополнительные сведения о замыкании конвейера см. в
разделе Создание конвейера ПО промежуточного слоя с помощью
IApplicationBuilder.
ПО промежуточного
слоя
Описание
Номер
Authentication
Обеспечивает
поддержку проверки
Ставится перед тем, как потребуется
HttpContext.User . Является конечным для
подлинности.
обратных вызовов OAuth.
Обеспечивает
поддержку
Непосредственно после ПО
промежуточного слоя для проверки
авторизации.
подлинности.
Позволяет
Перед ПО промежуточного слоя, которое
отслеживать согласие
использует файлы cookie. Примеры
пользователей на
хранение личных
Authentication, Session, MVC (TempData).
Авторизация
Политика Cookie
сведений и
применять
минимальные
стандарты для полей
файлов cookie, таких
как secure и
SameSite .
CORS
DeveloperExceptionPage
Настраивает общий
доступ к ресурсам
Ставится перед компонентами,
использующими CORS. UseCors сейчас
независимо от
источника.
нужно использовать перед
Создает страницу со
сведениями об
Ставится перед компонентами,
выдающими ошибки. Шаблоны проектов
ошибках,
автоматически регистрируют это ПО
предназначенными
для использования
промежуточного слоя в качестве первого
в конвейере в среде разработки.
только в среде
разработки.
UseResponseCaching из-за этой ошибки
.
ПО промежуточного
слоя
Описание
Номер
Error Handling
Отдельное ПО
Ставится перед компонентами,
промежуточного
слоя, которое
выдающими ошибки. Является конечным
для исключений или обслуживания веб-
обеспечивает
обработку
страницы по умолчанию для новых
приложений.
исключений,
предоставляет
страницу исключений
для разработчика,
страницы состояния
кода, веб-страницу
по умолчанию для
новых приложений.
Forwarded Headers
Пересылает
заголовки,
Перед компонентами, использующими
обновленные поля. Например: схема,
переданные через
прокси-сервер, в
узел, IP-адрес клиента, метод.
текущий запрос.
Проверка
Проверяет
Является конечным, если запрос
работоспособности
работоспособность
соответствует конечной точке проверки
приложения ASP.NET
Core и его
работоспособности.
зависимостей, таких
как проверка
доступности базы
данных.
Распространение
Распространяет
заголовков
заголовки HTTP из
входящего запроса
на исходящие
запросы HTTPклиентов.
Ведение журнала HTTP
Регистрирует
запросы и отклики
В начале конвейера ПО промежуточного
слоя.
HTTP.
HTTP Method Override
Разрешает входящий
Ставится перед компонентами,
запрос POST для
переопределения
использующими обновленный метод.
этого метода.
ПО промежуточного
слоя
Описание
Номер
HTTPS Redirection
Перенаправляет все
запросы HTTP на
Ставится перед компонентами,
использующими URL-адрес.
HTTPS.
HTTP Strict Transport
Security (HSTS)
ПО промежуточного
слоя для повышения
Перед отправкой ответов и после
компонентов, изменяющих запросы.
безопасности,
которое добавляет
Примеры Forwarded Headers, URL
Rewriting.
специальный
заголовок ответа.
MVC
Обрабатывает
Является конечным, если запрос
запросы с помощью
MVC либо Razor
соответствует маршруту.
Pages.
OWIN
Взаимодействие с
Является конечным, если ПО
приложениями,
промежуточного слоя OWIN полностью
серверами и ПО
промежуточного
обрабатывает запрос.
слоя на основе OWIN.
Кэширование
Обеспечивает
Ставится перед компонентами,
выходных данных
поддержку
кэширования ответов
требующими кэширование. UseRouting
Кэширование ответов
на основе
нужно использовать перед
UseOutputCaching . UseCORS нужно
конфигурации.
использовать перед UseOutputCaching .
Обеспечивает
Ставится перед компонентами,
поддержку для
кэширования
требующими кэширование. UseCORS
откликов. Для этого
требуется участие
полезно для приложений
клиента для работы.
Используйте
пользовательского интерфейса, таких как
Страницы, так как Razor браузеры обычно
кэширование
задают заголовки запросов, которые
предотвращают кэширование.
выходных данных для
полного управления
Распаковка запросов
нужно использовать перед
UseResponseCaching . Обычно это не
Кэширование выходных данных дает
сервером.
преимущества приложениям
пользовательского интерфейса.
Обеспечивает
поддержку
Ставится перед компонентами, которые
считывают текст запроса.
распаковки запросов.
ПО промежуточного
слоя
Описание
Номер
Сжатие откликов
Обеспечивает
поддержку для
Ставится перед компонентами,
требующими сжатие.
сжатия откликов.
Localization
Обеспечивает
поддержку
Ставится перед компонентами, для
которых важна локализация. Требуется
локализации.
отображение после ПО промежуточного
слоя маршрутизации при использовании
RouteDataRequestCultureProvider.
Маршрутизация
Определяет и
Является конечным для совпадающих
конечных точек
ограничивает
маршрутов.
маршруты запросов.
Безопасная проверка
Обрабатывает все
В конце цепочки, чтобы другое ПО
пароля
запросы от этой
точки в цепочке ПО
промежуточного слоя для обслуживания
статических файлов, действий MVC и т. д.
промежуточного
слоя, возвращая
имело приоритет.
страницу по
умолчанию для
одностраничного
приложения (SPA)
Session
Обеспечивает
Ставится перед компонентами,
поддержку для
требующими сеанс.
управления
пользовательскими
сеансами.
Static Files
Обеспечивает
Является конечным, если запрос
поддержку для
обработки
соответствует файлу.
статических файлов и
просмотра каталогов.
Переопределение URL-
Обеспечивает
Ставится перед компонентами,
адресов
поддержку для
переопределения
использующими URL-адрес.
URL-адресов и
перенаправления
запросов.
ПО промежуточного
слоя
Описание
Номер
W3CLogging
Создает журналы
доступа к серверу в
В начале конвейера ПО промежуточного
слоя.
расширенном
формате файла
журнала W3C .
WebSockets
Обеспечивает
поддержку
Ставится перед компонентами, которым
нужно принимать запросы WebSocket.
протокола
WebSockets.
Дополнительные ресурсы
Параметры времени существования и регистрации — раздел содержит
полный пример ПО промежуточного слоя со службами, имеющими время
существования scoped (с заданной областью), transient (временное) и singleton
(отдельное).
Написание пользовательского ПО промежуточного слоя ASP.NET Core
Тестирование ПО промежуточного слоя ASP.NET Core
Настройка gRPC-Web в ASP.NET Core
Перенос обработчиков HTTP-данных и модулей HTTP в ПО промежуточного
слоя ASP.NET Core
Запуск приложения в ASP.NET Core
Функции запросов в ASP.NET Core
Активация ПО промежуточного слоя на основе фабрики в ASP.NET Core
Активация ПО промежуточного слоя с помощью контейнера сторонних
разработчиков в ASP.NET Core
Универсальный узел .NET в
ASP.NET Core
Статья • 28.01.2023 • Чтение занимает 31 мин
В этой статье приведены сведения об использовании универсального узла .NET в
ASP.NET Core.
С помощью шаблонов ASP.NET Core создаются WebApplicationBuilder и
WebApplication, которые обеспечивают упрощенный способ настройки и запуска
веб-приложений без класса Startup . Дополнительные сведения о
WebApplicationBuilder и WebApplication см. в статье WebApplicationBuilder .
Сведения об использовании универсального узла .NET в консольных приложениях
см. в статье Универсальный узел .NET.
Определение узла
Узел — это объект, который инкапсулирует все ресурсы приложения, такие как:
Внедрение зависимостей
Ведение журнала
Параметр Configuration
Реализации IHostedService
После запуска узла он вызывает IHostedService.StartAsync в каждой реализации
IHostedService, зарегистрированной в коллекции размещенных служб контейнера
службы. В веб-приложении одна из реализаций IHostedService является вебслужбой, которая запускает IHostedService .
Включение всех взаимозависимых ресурсов приложения в один объект позволяет
контролировать запуск приложения и корректное завершение работы.
Создание узла
Узел обычно настраивается, собирается и выполняется кодом в классе Program.cs .
С помощью следующего кода создается узел с одной реализацией IHostedService ,
добавленной в контейнер внедрения зависимостей:
C#
await Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<SampleHostedService>();
})
.Build()
.RunAsync();
Для рабочей нагрузки HTTP вызовите ConfigureWebHostDefaults после
CreateDefaultBuilder:
C#
await Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build()
.RunAsync();
Параметры построителя по умолчанию
Метод CreateDefaultBuilder:
В качестве корневого каталога содержимого задает путь, возвращенный
методом GetCurrentDirectory.
Загружает конфигурацию узла из:
Переменные среды с префиксом DOTNET_ .
аргументы командной строки.
Загружает конфигурацию приложения из:
appsettings.json .
appsettings.{Environment}.json .
секреты пользователя, когда приложение выполняется в среде ;
Переменные среды.
аргументы командной строки.
Добавляет следующие регистраторы:
Консоль
Отладка
EventSource
Журнал событий (только при запуске в Windows)
Включает проверку области и проверку зависимостей, если это среда
разработки.
Метод ConfigureWebHostDefaults:
Загружает конфигурацию узла из переменных среды с префиксом
ASPNETCORE_ .
Задает сервер Kestrel в качестве веб-сервера и настраивает его с помощью
поставщиков конфигурации размещения приложения. Параметры сервера
Kestrel по умолчанию см. в статье Kestrel.
Добавляет ПО промежуточного слоя фильтрации узлов.
Добавляет Параметры ПО промежуточного слоя перенаправления
заголовков, если равно true .
Обеспечивает интеграцию служб IIS. Параметры служб IIS по умолчанию см. в
статье Размещение ASP.NET Core в Windows со службами IIS.
Разделы Параметры для всех типов приложений и Параметры для веб-приложений
далее в этой статье описывают, как переопределить параметры построителя по
умолчанию.
Платформенные службы
Следующие службы регистрируются автоматически.
IHostApplicationLifetime
IHostLifetime
IHostEnvironment / IWebHostEnvironment
Дополнительные сведения о службах, предоставляемых платформой, см. в разделе
Внедрение зависимостей в ASP.NET Core.
IHostApplicationLifetime
Внедрите IHostApplicationLifetime (прежнее название — IApplicationLifetime ) в
любой класс для выполнения задач после запуска и корректного завершения
работы. Три свойства этого интерфейса представляют собой токены отмены,
которые служат для регистрации методов обработчика событий запуска и
завершения работы приложения. Кроме того, интерфейс включает метод
StopApplication , который позволяет приложениям запрашивать корректное
завершение работы.
При корректном завершении работы узел выполняет следующие задачи:
Запускает обработчики событий ApplicationStopping, которые позволяют
приложению запускать логику до начала процесса завершения работы.
Останавливает работу сервера, который отключает новые подключения.
Сервер ожидает завершения запросов к существующим подключениям, пока
позволяет время ожидания завершения работы. Сервер отправляет заголовок
закрытия подключений для дальнейших запросов к существующим
подключениям.
Запускает обработчики событий ApplicationStopped, которые позволяют
приложению запускать логику после завершения его работы.
Ниже приведен пример реализации IHostedService , которая регистрирует
обработчики событий IHostApplicationLifetime :
C#
public class HostApplicationLifetimeEventsHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public HostApplicationLifetimeEventsHostedService(
IHostApplicationLifetime hostApplicationLifetime)
=> _hostApplicationLifetime = hostApplicationLifetime;
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
_hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
_hostApplicationLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
private void OnStarted()
{
// ...
}
private void OnStopping()
{
// ...
}
private void OnStopped()
{
// ...
}
}
IHostLifetime
Реализация IHostLifetime контролирует, когда узел запускается и останавливается.
Используется последняя зарегистрированная реализация.
Microsoft.Extensions.Hosting.Internal.ConsoleLifetime — это реализация
IHostLifetime по умолчанию. ConsoleLifetime :
Ожидает передачи данных с использованием сигналов
(Windows),
⌘
C
CTRL
(macOS) или SIGTERM и вызывает метод
C
/SIGINT
для запуска
процесса завершения работы.
разблокирует расширения, такие как RunAsync и WaitForShutdownAsync.
IHostEnvironment
Внедряет службу IHostEnvironment в класс, чтобы получить сведения о следующих
параметрах.
ApplicationName
EnvironmentName
ContentRootPath
Веб-приложения реализуют интерфейс IWebHostEnvironment , который наследует
IHostEnvironment и добавляет IWebHostEnvironment .
Конфигурация узла
Конфигурация узла используется для свойств реализации IHostEnvironment.
Конфигурация узла доступна в HostBuilderContext.Configuration внутри
ConfigureAppConfiguration. После
ConfigureAppConfiguration HostBuilderContext.Configuration заменяется
конфигурацией приложения.
Чтобы добавить конфигурацию узла, вызовите ConfigureHostConfiguration в
IHostBuilder . Метод ConfigureHostConfiguration может вызываться несколько раз с
накоплением результатов. Узел использует значение, заданное последним для
данного ключа.
Поставщик переменных среды с префиксом DOTNET_ и аргументы командной
строки включены в CreateDefaultBuilder . Для веб-приложений добавляется
поставщик переменных среды с префиксом ASPNETCORE_ . Префикс удаляется при
чтении переменных среды. Например, значение переменной среды для
ASPNETCORE_ENVIRONMENT становится значением конфигурации узла для ключа
environment .
В следующем примере создается конфигурация узла:
C#
Host.CreateDefaultBuilder(args)
.ConfigureHostConfiguration(hostConfig =>
{
hostConfig.SetBasePath(Directory.GetCurrentDirectory());
hostConfig.AddJsonFile("hostsettings.json", optional: true);
hostConfig.AddEnvironmentVariables(prefix: "PREFIX_");
hostConfig.AddCommandLine(args);
});
Конфигурация приложения
Конфигурация приложения создается путем вызова метода
ConfigureAppConfiguration в IHostBuilder . Метод ConfigureAppConfiguration может
вызываться несколько раз с накоплением результатов. Приложение использует
значение, заданное последним для данного ключа.
Конфигурация, созданная с помощью ConfigureAppConfiguration , доступна в
свойствах HostBuilderContext.Configuration для последующих операций и как
служба из внедрения зависимостей. Конфигурация узла также добавляется к
конфигурации приложения.
Дополнительные сведения см. в разделе Конфигурация в ASP.NET Core.
Параметры для всех типов приложений
В этом разделе перечислены параметры узла, которые применяются к рабочим
нагрузкам HTTP и остальным. По умолчанию переменные среды, используемые для
настройки этих параметров, могут иметь префикс DOTNET_ или ASPNETCORE_ ,
который отображается в следующем списке параметров в качестве заполнителя
{PREFIX_} . Дополнительные сведения см. в разделе Параметры построителя по
умолчанию и разделе "Переменные среды" статьи "Конфигурация".
ApplicationName
Свойство IHostEnvironment.ApplicationName задается в конфигурации узла во
время создания узла.
Ключ:
Тип: string
По умолчанию: Имя сборки, содержащей точку входа приложения.
Переменная среды:
Чтобы задать это значение, используйте переменную среды.
ContentRoot
Свойство IHostEnvironment.ContentRootPath определяет, где узел начинает искать
файлы содержимого. Если путь не существует, узел не запускается.
Ключ:
Тип: string
По умолчанию: папка, в которой находится сборка приложения.
Переменная среды:
Чтобы задать это значение, используйте переменную среды или вызов
UseContentRoot в IHostBuilder :
C#
Host.CreateDefaultBuilder(args)
.UseContentRoot("/path/to/content/root")
// ...
Дополнительные сведения можно найти в разделе
Корневой каталог содержимого
Корневой каталог документов
EnvironmentName
Свойство IHostEnvironment.EnvironmentName может иметь любое значение. В
платформе определены значения Development , Staging и Production . Регистр
символов в значениях не учитывается.
Ключ:
Тип: string
По умолчанию: Production
Переменная среды:
Чтобы задать это значение, используйте переменную среды или вызов
UseEnvironment в IHostBuilder :
C#
Host.CreateDefaultBuilder(args)
.UseEnvironment("Development")
// ...
ShutdownTimeout
HostOptions.ShutdownTimeout задает время ожидания для StopAsync. Значение по
умолчанию — пять секунд. Во время ожидания узел:
Активирует IHostApplicationLifetime.ApplicationS
Download