Часть I. Введение в инфраструктуру ASP.NET Core MVC
Глава 2. Ваше первое приложение МУС
Глава 3. Паттерн МУС, проекты и соглашения
Глава 4. Важные функциональные возможности языка C#
Глава 5. Работа с Razor
Глава 6. Работа с Visual Studio
Глава 7. Модульное тестирование приложений МУС
Глава 8. SportsStore: реальное приложение
Глава 9. SportsStore: навигация
Глава 10. SportsStore: завершение построения корзины для покупок
Глава 11. SportsStore: администрирование
Глава 12. SportsStore: защита и развертывание
Глава 13. Работа с Visual Studio Code
Часть II. Подробные сведения об инфраструктуре ASP.NET Core MVC
Глава 15. Маршрутизация URL
Глава 16. Дополнительные возможности маршрутизации
Глава 17. Контроллеры и действия
Глава 18. Внедрение зависимостей
Глава 19. Фильтры
Глава 20. Контроллеры API
Глава 21. Представления
Глава 22. Компоненты представлений
Глава 23. Дескрипторные вспомогательные классы
Глава 24. Использование дескрипторных вспомогательных классов для форм 741 Глава 25. Использование других встроенных дескрипторных вспомогательных классов
Глава 26. Привязка моделей
Глава 27. Проверка достоверности моделей
Глава 28. Введение в ASP.NET Core Identity
Глава 29. Применение ASP.NET Core Identity
Глава 30. Расширенные средства ASP.NET Core Identity
Глава 31. Соглашения по модели и ограничения действий
Предметный указатель
Text
                    ASP.NET CORE MVC
с примерами на С#
ДЛЯ ПРОФЕССИОНАЛОВ
6-е издание


Pro ASP.NET Core MVC Sixth Edition Adam Freeman
ASP.NET Core MVC с примерами на С# ДЛЯ ПРОФЕССИо·НАЛОВ 6-е издание Адам Фримен Москва· Санкт-Петербург· Киев 2017
ББК 32.973.26-018.2 .75 Ф88 УДК 681.3.07 Компьютерное издательство "Диалектика" Зав. редакцией С.Н. Тригуб Перевод с английского Ю.Н. Артеменко Под редакцией Ю.Н. Артеменко По общим вопросам обращайтесь в издательство "Диалектика" по адресу: info@dialektika.com,http://www.dialektika.com Фримен, Адам. Ф88 ASP.NET Core MVC с примерами на С# для профессионалов , 6-е изд. : Пер. с англ. - СпБ. : ООО "Альфа-книга", 2017. - 992 с. : ил. - Парал. тит. англ. ISBN 978-5-9908910-4-3 (рус .) ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми мар­ ками соответствующих фирм . Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носит ель, если на это н ет письменного разрешения издательст в а APгess, Berkel ey, СА. Authoriz ed translatioп from the Eпglish laпguage editioп puЬlished Ьу APress , !пс " Copyright © 2016 Ьу Adam Fr ee maп. All rights reserved. No part of this work тау Ье reproduc ed or traпsmitted lп апу form or Ьу апу meaпs, electroпic or mechaпical , lпcludiпg photocopylпg, recordlng, or Ьу any lnformatioп storage or retri eva l system, without the prior wrltteп permissioп of the copyright owner апd the puЬlish e r. 1Тademarked names may appear in this book. Rather th aп use а trademark symbol with every оссurгепсе of а trademarked пате , we use th e п ames опlу iп ап editorial fashioп апd to tl1e beп ­ efit of the trademark owпer. with по iпteпtloп of iпfringemeпt of the trademark. Russiaп laпguage editioп is puЬlished Ьу Dialektika Computer Books PuЬlishiпg according to the Agreemeпt with R&I Eпterprises Iпterпatioпal , Copyright © 2017. Научно-популярное издание Адам Фримен ASP.NET Core МVС с примерами на С# для профессионалов 6-е издание Верстка Художественный редактор Т.Н. Артеменко В.Г. Павлютин Подписано в печать 23.01.2017. Формат 70Х100/ 16. Гарнитура Тimes. Печать офсетная. Усл. печ. л. 79,98. Уч.-изд. л. 58,4. ТИраж 500 энз. Заназ No 648. Отпечатано способом ролевой струйной печати в АО • Первая Образцовая типография • Филиал • Чеховский Печатный Двор » 142300, Московская область, г. Чехов, ул. Полиграфистов, д. l ООО "Альф а-ннига", 195027, Саннт-Петербург, Магнитогорская ул " д. 30 ISBN 978-5 -9908910-4-3 (рус.) ISBN 978-1 -484-20398-9 (англ.) © Компьютерное издательство "Диалентина ", 2017 © Ьу Adam Freeman , 2017
Оглавление Часть 1. Введение в инфраструктуру ASP.NET Core MVC 19 Глава 1. Основы ASP.NET Core MVC 20 Глава 2. Ваше первое приложение MVC 29 Глава 3. Паттерн MVC, проекты и соглашения 68 Глава 4. Важные функциональные возможности языка С# 82 Глава 5. Работа с Razor 115 Глава 6. Работа с Visual Studio 136 Глава 7. Модульное тестирование приложений MVC 170 Глава 8. SportsStore: реальное приложение 201 Глава 9. SportsStore: навигация 244 Глава 10. SportsStore: завершение построения корзины для покупок 276 Глава 11. SportsStore: администрирование 298 Глава 12. SportsStore: защита и развертывание 325 Глава 13. Работа с Visual Studio Code 348 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 373 Глава 14. Конфигурирование приложений 374 Глава 15. Маршрутизация URL 426 Глава 16. Дополнительные возможности маршрутизации 465 Глава 17. Контроллеры и действия 500 Глава 18. Внедрение зависимостей 543 Глава 19. Фильтры 576 Глава 20. Контроллеры API 614 Глава 21. Представления 644 Глава 22. Компоненты представлений 678 Глава 23. Дескрипторные вспомогательные классы 708 Глава 24. Использование дескрипторных вспомогательных классов для форм 741 Глава 25. Использование других встроенных дескрипторных вспомогательных классов Глава 26. Привязка моделей Глава 27. Проверка достоверности моделей Глава 28. Введение в ASP.NET Core ldeпtity Глава 29. Применение ASP.NET Core ldentity Глава 30. Расширенные средства ASP.NET Core ldentity Глава 31. Соглашения по модели и ограничения действий Предметный указатель 766 793 828 861 898 926 958 987
Содержание Об авторе Часть 1. Введение в инфраструктуру ASP.NET Core MVC Глава 1. Основы ASP.NET Core MVC История развития ASP. NЕТ Core МVС ASP.NEТWeb Foгms Первоначальная инфраструктура МVС Fгamewoгk Обзор ASP.NET Core Основные преимущества ASP.NET Core МVС Архитектура МVС Расширяемость Жесткий контроль над HTML и НТТР Тестируемость Мощная система маршрутизации Современный АРI-интерфейс Межплатформенная природа Инфраструктура ASP. NEТ Core МVС имеет открытый код Что необходимо знать? Канава структура книги? Часть I. Введение в инфраструктуру ASP.NEТ Соге МVС Часть П . Подробные сведения об инфраструктуре ASP.NEТ Core МVС Что нового в этом издании? Iде можно получить код примеров? Резюме Глава 2. Ваше первое приложение MVC Установка Visual Studio Создание нового проекта ASP. NЕТ Саге МVС Добавление первого контроллера Понятие маршрутов Визуализация неб - страниц Создание и визуализация представления Добавление динамического вывода Создание простого приложения для ввода данных Предварительная настройка Проектирование модели данных Создание второго действия и строго типизированного представл е ния Ссылка на методы действий Построение формы Получение данных формы Добавление проверки достоверности Стилизация содержимого Резюме 18 19 20 20 21 22 23 24 24 24 25 25 25 26 26 27 27 27 27 28 28 28 28 29 29 31 33 36 37 37 40 42 42 43 44 46 47 49 56 61 67
Содержание 7 Глава З. Паттерн MVC, проекты и соглашения 68 История создания МVС 68 Особенности паттерна МVС 68 Понятие моделей 69 Понятие контроллеров 70 Понятие представлений 70 Реализация MVC в ASP.NEТ Core 70 Сравнение MVC с другими паттернами 71 Паттерн интеллектуального пользовательского интерфейса 71 Проекты ASP. NEТ Core МVС 75 Создание проекта 76 Соглашения в проекте МVС 79 Резюме 81 Глава 4. Важные функциональные возможности языка С# 82 Подготовка проекта для примера 83 Включение ASP. NЕТ Core МVС 84 Создание компонентов приложения МVС 85 Использование nul l-условной операции 87 Связывание в цепочки nu ll-условных операций 88 Комбинирование nul l -условной операции и операции объединения с n u ll 89 Использование автоматически реализуемых свойств 90 Использование инициализаторов автоматически реализуемых свойств 91 Со здание автоматически реализуемых свойств только для чтения 92 Использование интерполяции строк 93 Использование инициализаторов объектов и коллекций 94 Использование инициализатора индексированной коллекции 96 Использование расширяющих методов 97 Применение расширяющих методов к интерфейсу 98 Создание фильтрующих расширяющих методов l00 Использование лямбда- выражений 101 Определени е функций l03 Использование методов и свойств в форме лямбда-выражений 105 Использование автоматического выведения типа и анонимных типов l07 Использование анонимных типов l08 Использование асинхронных методов l09 Работа с задачами напрямую 110 Применение ключевых слов a sync и aw a i t 111 Получение имен 113 Резюме 114 Глава 5. Работа с Razor 115 Подготовка проекта для примера 116 Определение модели 117 Создание контроллера 117 Создание представления 118 Работа с объектом модели 119 Использование файла импортирования представлений 121 Работа с компоновками 123 Создание компоновки 123
8 Содержание Применение компоновки Использование файла запуска представления Использование выражений Razor Вставка значений данных Установка значений атрибутов Использование условных операторов Проход по содержимому массивов и коллекций Резюме Глава 6. Работа с Visual Studio Подготовка проекта для примера Создание модели Создание контроллера и представления Управление программными пакетами Инструмент NuGet Инструмент Bower Итеративная разработка Внесение изменений в представления Razoг Внесение изменений в классы С# Использование средства Вгоwsег Link Подготовка файлов JavaScript и CSS для развертывания Включение доставки статического содержимого Добавление в прое1tт статического содержимого Обновление представления Пакетирование и минификация в приложениях МVС Резюме Глава 7. Модульное тестирование приложений MVC Подготовка проекта для примера Включение встроенных дескрипторных вспомогательных классов Добавление действий к контроллеру Создание формы для ввода данных Обновление представления Index Модульное тестирование приложений МVС Создание проекта модульного тестирования Изолирование компонентов для модульного тестирования Улучшение модульных тестов Параметризация модульного теста Улучшение фиктивных реализаций Резюме Глава 8. SportsStore: реальное приложение Начало работы Создание проекта МVС Создание проекга модульного тестирования Проверка и запуск приложения Начало работы с моделью предметной области Создание хранилища Создание фиктивного хранилища Регистрация службы хранилища 125 126 128 129 130 131 133 135 136 137 137 139 141 141 143 147 147 148 156 161 161 162 164 166 169 170 171 171 172 172 173 174 175 182 190 190 194 200 201 202 202 207 209 209 210 210 211
Отображение списка товаров Добавление контроллера Добавление и конфигурирование представления Установка стандартного маршрута Запуск приложения Подготовка базы данных Установка Entity Fгarnework Core Создание классов базы данных Создание юшсса хранилища Определение строки подключения Конфигурирование приложения Создание и применение миграции базы данных Добавление поддержки разбиения на страницы Отображение ссьmок на страницы Улучшение URL Стилизация содержимого Установка пакета Bootstrap Применение стилей Bootstrap к компоновке Создание частичного представления Резюме Глава 9. SportsStore : навигация Добавление навигационных элементов управления Фильтрация списка товаров Улучшение схемы URL Построение меню навигации по категориям Корректировка счетчика страниц Построение корзины для покупок Определение модели корзины Создание кнопок добавления в корзину Включение поддержки сеансов Реализация контроллера для корзины Отображение содержимого корзины Резюме Глава 1О. SportsStore: завершение построения корзины для покупок Усовершенствование модели корзины с помощью службы Создание класса корзины, осведомленного о хранилище Регистрация службы Упрощение контроллера Cart Завершение функциональности корзины Удаление элементов из корзины Добавление виджета с итоговой информацией по корзине Отправка заказов Создание класса модели Добавление реализации процесса оплаты Реализация обработки заказов Завершение построения контроллера Order Отображение сообщений об ошибках проверки достоверности Отображение итоговой страницы Резюме Содер жа ние 9 212 213 215 216 217 218 219 220 221 222 223 225 226 228 236 237 238 238 241 242 244 244 244 248 252 259 262 262 266 267 269 272 275 276 276 276 277 278 279 279 281 284 284 285 287 291 295 296 297
1О Содержание Глава 11. SportsStore: администрирование Управление заказами Расширение модели Добавление действий и представления Добавление средств управления каталогом Создание контроллера CRUD Реализация представления списка Редактирование сведений о товарах Создание новых товаров Удаление товаров Резюме Глава 12 . SportsStore: защита и развертывание Защита средств администрирования Добавление в проект пакета Jdentity Создание базы данных Identity Применение базовой политики авторизации Создание контроллера Accoun t и представлений Тестирование политики безопасности Развертывание приложения Создание баз данных Подготовка приложения Применение миграций баз данных Процесс развертывания приложения Резюме Глава 13 . Работа с Visual Studio Code Настройка среды разработки Установка Node.js Проверка установки Node Установка Git Проверка установки Git Установка Yeoman, Bower и Gulp Установка .NETCore Проверка установки .NЕТ Core Установr\а Visual Studio Code Проверка установки Visual Studio Code Установка расширения С# для Visual Studio Code Создание проекта ASP. NET Core Подготовка проекта с помощью Visual Studio Code Добавление в проект пакетов NuGet Добавление в проект пакетов клиентской стороны Конфигурирование приложения Построение и запуск проекта Воссоздание приложения Partylnvites Создание модели и хранилища Создание базы данных Создание контроллеров и представлений Модульное тестирование в Visual Studio Code Конфигурирование приложения Создание модульного теста Прогон тестов Резюме 298 298 298 299 302 303 304 306 320 321 324 325 325 325 326 330 332 336 336 337 338 343 343 346 348 348 349 350 350 351 351 351 352 353 353 353 355 356 357 358 359 359 360 360 363 365 369 369 371 371 372
Содержание 11 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 373 Глава 14. Конфигурирование приложений 374 Подготовка проекта для примера 376 Конфигурационные файлы JSON 377 Конфигурирование решения 378 Конфигурирование проекта 380 Класс Prograrn 383 Класс Startup 385 Особенности использования класса Startup 386 Службы ASP.NEТ 387 Промежуточное программное обеспечение ASP.NEТ 390 Особенности вызова метода Configu re () 400 Добавление оставшихся компонентов промежуточного программного обеспечения 408 Использование данных конфигурации 413 Конфигурирование служб МVС 419 Работа со сложными конфигурациями 421 Создание разных внешних конфигурационных файлов 421 Создание разных методов конфигурирования 422 Создание разных классов конфигурирования 424 Резюме 425 Глава 15. Маршрутизация URL 426 Подготовка проекта для примера 428 Создание класса модели 429 Создание контроллеров 430 Создание представления 431 Введение в шаблоны URL 432 Создание и регистрация простого маршрута 434 Определение стандартных значений 435 Определение встроенных стандартных значений 436 Использование статических сегментов URL 438 Определение специальных переменных сегментов 442 Использование специальных переменных в качестве параметров метода действия 445 Определение необязательных сегментов URL 446 Определение маршрутов переменной длины 448 Ограничение маршрутов 451 Ограничение маршрута с использованием регулярного выражения 454 Использование ограничений на основе типов и значений 455 Объ единение ограничений 456 Определение специального ограничения 458 Использование маршрутизации с помощью атрибутов 460 Подготовка для маршрутизации с помощью атрибутов 460 Применение маршрутизации с помощью атрибутов 461 Применение ограничений к маршрутам 463 Резюме 464
12 Содержание Глава 16. Дополнительные возможности маршрутизации Подготовка проекта для примера Генерация исходящих URL в представлениях генерирование исходящих ссылок Генерация URL (без ссылок) генерирование URL в методах действий Настройка системы маршрутизации Изменение конфигурации системы маршрутизации Создание специального класса маршрута Применение специального класса маршрута Маршрутизация на контроллеры МVС Работа с областями Создание области Создание маршрута для области Заполнение области Генерирование ссылок на действия в областях Полезные советы относительно схемы URL Делайте URL чистыми и понятными человеку GET и POST: выбор правильного запроса Резюме Глава 17. Контроллеры и действия Подготовка проекта для примера Подготовка представлений Поняти е контроллеров Создание контроллеров Создание контроллеров РОСО Использование базового класса Controller Получение данных контекста Получение данных из объектов контекста Использование параметров метода действия генерирование ответа Генерирование ответа с использованием объекта контекста Понятие результатов действий Генерирование НТМL-ответа Передача данных из метода действия в представление Выполнение перенаправления Возвращение разных типов содержимого Реагирование с помощью содержимого файлов Возвращение ошибок и кодов Н1ТР Другие классы результатов действий Резюме Глава 18. Внедрение зависимостей Подготовка проекта для прим ера Создание модели и хранилища Создание контроллера и представления Создание проекта модульного тестирования Создание слабо связанных компонентов Исследование сильно связанных компонентов 465 467 468 468 478 478 479 479 481 484 485 491 492 492 493 496 497 497 499 499 500 501 503 505 506 506 508 509 510 514 516 516 517 520 523 528 535 538 540 542 542 543 544 545 547 548 549 550
Содержание 13 Введение в средство внедрения зависимостей ASP. NET 556 Подготовка к внедрению зависимостей 556 I\онфигурирование поставщика служб 558 Модульное тестирование контроллера с зависимостью 560 Использование цепочек зависимостей 560 Использование внедрения зависимостей для конкретных типов 563 Жизненные ЦИIUIЫ служб 565 Использование переходного жизненного цикла 566 Использование жизненного цикла, ограниченного областью действия 570 Использование жизненного цикла одиночки 571 Использование внедрения в действия 572 Использование атрибутов внедрения в свойства 573 Запрашивание объекта реализации вручную 574 Резюме 575 Глава 19. Фильтры 576 Подготовка проекта для примера 577 Включение SSL 579 Создание контроллера и представления 579 Использование фильтров 581 Понятие фильтров 584 Получение данных контекста 585 Использование фильтров авторизации 585 Создание фильтра авторизации 586 Использование фильтров действий 588 Создание фильтра действий 590 Создание асинхронного фильтра действий 591 Использование фильтров результатов 592 Создание фильтра результатов 593 Создание асинхронного фильтра результатов 595 Создание гибридного фильтра действий/результатов 596 Использование фильтров исIUiючений 598 Создание фильтра исключений 599 Использование внедрения зависимостей для фильтров 600 Распознавание зависимостей в фильтрах 601 Управление жизненными циклами фильтров 605 Создание глобальных фильтров 608 Порядок применения фильтров и его изменение 61О Изменения порядка применения фильтров 612 Резюме 613 Глава 20. Контроллеры API 614 Подготовка проекта для примера 615 Создание модели и хранилища 615 Создание контроллера и представлений 617 Конфигурирование приложения 619 Роль контроллеров REST 621 Проблема скорости 621 Проблема эффективности 622 Проблема открытости 622
14 Содержание Введение в RESТ и контроллеры АР! Создание контроллера АР! Тестирование контроллера АР! Использование контроллера АР! в браузере Форматирование содержимого Стандартная политика содержимого Согласование содержимого Указание формата данных для действия Получение формата данных из маршрута или строки запроса Включение полного согласования содержимого Получение разных форматов данных Резюме Глава 21. Представления Подготовка проекта для примера Создание специального механизма визуализации Создание специальной реализации интерфейса I View Создание реализации интерфейса IViewEngine Регистрация специального механизма визуализации Тестирование механизма визуализации Работа с механизмом Razor Подготовка проекта для примера Прояснение представлений Razor Добавление динамического содержимого к представлению Razor Использование разделов компоновки Использование частичных представлений Добавление содержимого JSON в представления Конфигурирование механизма Razor Расширители местоположений представлений Резюме Глава 22. Компоненты представлений Подготовка проекта для примера Создание моделей и хранилиш Создание контроллера и представлений Конфигурирование приложения Понятие компонентов представлений Создание компонента представления Создание компонентов представлений РОСО Наследование от базового класса ViewComp onent Понятие результатов компонентов представлений Получение данных контекста Создание асинхронных компонентов представлений Создание гибридных компонентов контроллеров/представлений Создание гибридных представлений Прим енение гибридного класса Резюме Глава 23. Дескрипторные вспомогательные классы Подготовка проекта для примера Создание модели и хранилища 623 623 628 631 634 635 636 639 639 641 643 643 644 645 647 649 650 651 652 654 654 656 660 661 666 669 671 672 677 678 679 680 682 685 686 686 686 688 690 695 701 703 704 706 707 708 709 710
Содержание 15 Создание контроллера, компоновки и представлений 711 Конфигурирование приложения 713 Создание дескрипторного вспомогательного класса 715 Определение дескрипторного вспомогательного класса 715 Регистрация дескрипторных вспомогательных классов 719 Использование дескрипторного вспомогательного класса 719 Управление областью действия дескрипторного вспомогательного класса 721 Усовершенствованные возможности дескрипторных вспомогательных классов 726 Создание сокращающих элементов 726 Вставка содержимого перед и после элементов 728 Получение данных контекста представления и использование внедрения зависимостей 732 Работа с моделью представления 734 Согласование дескрипторных вспомогательных классов 736 Подавление выходного элемента 738 Резюме 740 Глава 24. Использование дескрипторных вспомогательных классов для форм 7 41 Подготовка проекта для примера 743 Изменение регистрации дескрипторных вспомогательных классов 743 Переустановка представлений и компоновки 743 Работа с элементами f о rrn 745 Установка цели формы 746 Использование средства противодействия подделке 746 Работа с элементами input 749 Конфигурирование элементов input 749 Форматирование значений данных 751 Работа с элементами label 754 Работа с элементами select и option 756 Использование источника данных для заполнения элемента se lect 758 генерирование элементов option из перечисления 758 Работа с элементами textarea 763 Дескрипторные вспомогательные классы для проверки достоверности форм 764 Резюме 765 Глава 25. Использование других встроенных дескрипторных вспомогательных классов Подготовка проекта для примера Использование вспомогательного класса для среды размещения Использование вспомогательных классов для JavaScript и CSS Управление файлами JavaScript Управление таблицами стилей CSS Работа с якорными элементами Работа с элементами i rng Использование кеша данных Установка времени истечения для кеша Использование вариаций кеша Использование URL, относительных к приложению Резюме 766 767 768 769 769 778 781 782 783 786 788 789 792
16 Содержание Глава 26. Привязка моделей 793 Подготов:ка прое:кта для примера 794 Создание модели и хранилища 795 Создание контроллера и представления 796 Конфигурирование приложения 798 Понятие привязки моделей 799 Стандартные значения привязки 80l Привязка простых типов 802 Привязка сложных типов 803 Привязка массивов и коллекций 813 Привязка коллекций сложных типов 817 Указание источника данных привяз:ки моделей 820 Выбор стандартного источника данных привяз:ки 820 Использование заголовков в качестве источников данных привязки 821 Использование тел запросов в качестве источни:ков данных привязки 824 Резюме 827 Глава 27 . Проверка достоверности моделей 828 Подготовка проекта для примера 830 Создание модели 831 Создание контроллера 831 Создание компоновки и представлений 832 Необходимость в проверке достоверности модели 834 Явная проверка достоверности модели 835 Отображение пользователю ошибок проверки достоверности 838 Отображение сообщений об ошибках проверки достоверности 839 Отображение сообщений об ошибках проверки достоверности на уровне свойств 844 Отображение сообщений об ошибках проверки достоверности на уровне модели 845 Указание правил проверки достоверности с помощью метаданных 849 Создание специального атрибута проверки достоверности для свойства 852 Выполнение проверки достоверности на стороне :клиента 854 Выполнение удаленной проверки достоверности 857 Резюме 860 Глава 28. Введение в ASP .NET Core ldentity 861 Подготовка проекта для примера 863 Создание контроллера и представления 864 Настрой:ка ASP. NЕТ Core Identity 866 Добавление пакета Identity в приложение 866 Создание :класса пользователя 867 Создание класса контекста базы данных 869 Конфигурирование настройки строки под:ключения к базе данных 869 Конфигурирование служб и промежуточного программного обеспечения Identity 870 Создание базы данных Identity 872 Использование ASP.NEТ Core Identity 872 Перечисление пользовательских учетных записей 873 Создание пользователей 875 Проверка паролей 879 Проверка деталей, связанных с пользователем 886
Завершение средств администрирования Реализация средства удаления Реализация возможности редантирования Резюме Глава 29. Применение ASP.NET Core ldentity Подготовка проекта для примера Аутентификация пользователей Подготовка к реализации аутентификации Добавление аутентификации пользователей Тестирование аутентификации Авторизация пользователей с помощью ролей Создание и удаление ролей Управление членством в ролях Использование ролей для авторизации Помещение в базу данных начальных данных Резюме Глава 30. Расширенные средства ASP.NET Core ldentity Подготовка проекта для примера Добавление специальных свойств в класс пользователя Подготовка миграции базы данных Тестирование специальных свойств Работа с заявками и политиками Понятие заявок Создание заявок Использование политю< Использование политик для авторизации доступа к ресурсам Использование сторонней аутентификации Регистрация приложения в Google Включение аутентификации Google Резюме Глава 31. Соглашения по модели и ограничения действий Подготовка проекта для примера Создание модели представления, контроллера и представления Использование модели приложения и соглашений по модели Модель приложения Роль соглашений по модели Создание соглашения по модели Порядок вьmолнения соглашений по модели Создание глобальных соглашений по модели Использование ограничений действий Подготовка проекта для примера Ограничения действий Создание ограничения действия Распознавание зависимостей в ограничениях действий Резюме Предметный указатель Содержание 17 890 891 892 897 898 898 899 901 904 907 908 909 914 919 923 925 926 927 927 931 931 932 933 937 940 946 951 951 952 957 958 959 960 962 963 967 968 973 974 976 977 978 979 984 986 987
Посвящается моей прекрасной жене Джеки Гриффиmс. Об авторе Адам Фримев - опытный специалист в области информационных технологий, занимавший ведущие позиции во многих компаниях, последней из которых был гло­ бальный банк, где он работал на должностях директора по внедрению технологий и руководителя административной службы. После ухода из банка Адам уделяет все свое время писательской деятельности и бегу на длинные дистанции . О техническом рецензенте Фабио Клау.цио Ферраччати - ведущий консультант и главный аналитик/раз­ работчик, исполь зующий технологии Microsoft. Он работает в компании Brain Force (www . Ыua r anc i o . com). Фабио является сертифицированным Microsoft разработчи­ ком реш ений для .NET (Microsoft Certified Solution Developer for .NET), сертифициро­ ванным Microsoft разработчиком приложений для .NET (Microsoft Certified Application Develop er for .NET), сертифицированным Microsoft профессионалом (Microsoft Certifled Professional), а также плодовитым автором и техническим рецензентом. За последние десять лет он написал множество статей для итальянских и международных журна­ лов и выступал в качестве соавтора в более чем 10 книгах по разнообразны м темам , связанным с 1щмпьютерами. Ждем ваших отзы вов! Вы, читатель этой книги , и есть главный ее критик. Мы ценим ваше мнение и хо­ тим знать, что было сдел ано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в наш адрес. Мы ждем ваших комментариев и надеемся на них . Вы можете прислать нам бу­ мажное или электронное письмо либо просто посетить наш веб-сайт и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать , нравится ли вам эта книга, а также выскажите свое мнение о том , как сделать наши книги более интересными для вас. Отпр авляя письмо или сообщение, не забудьте указать название книги и ее авто­ ров , а та кже свой обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию новых книг. Наши эл е ктронные адреса: E-mail: WWW: info@dialektika.com http : //www . dialektika . com Наши почтовые адр еса: в России: в Украине: 195027, Санкт-Петербург, Магнитогорская ул., д. 30 , ящик 116 03150, Киев, а/я 152
ЧАСТЬ 1 Введение в инфраструктуру ASP.NET Core МVС д ля разработчиков веб-приложений, использующих платформу Microsoft, инфраструктура ASP.NET Core MVC представляется как радикальное изме­ нен ие . Особое значение придается чистой архитектуре, паттернам проектирова­ ния и удобству тестирования , к тому же не предпринимается попытка скрывать, каким образом работает веб-среда. Первая часть этой книги поможет понять в общих чертах основополагающие идеи разработки приложений MVC, включая новые возможности ASP. NЕТ Core MVC, и увидеть на деле, как применяется инфраструктура .
ГЛАВА 1 Основы ASP.NET Core МVС ASP.NEТ Core MVC - это инфраструктура для разработки веб-приложений про­ изводства Micгosoft , которая сочетает в себе эффективность и аккуратность ар­ хитектуры "модель-представление-контроллер" (model-view-controller - MVC), идеи и приемы гибкой разработки. а также лучшие части платформы .NET . В настоящей главе вы узнаете, почему компания Micгosoft создала инфраструктуру ASP.NET Соге МVС, увидите, как она соотносится со своими предшественниками и альтернативами, а также ознакомитесь с обзором нововведений ASP.NET Core MVC и с тем, что будет рассматриваться в книге. История развития ASP.NET Core MVC Первая версия платформы ASP.NET появилась в 2002 году, в то время, когда компа­ ния Micгosoft стремилась сохранить господствующее положение в области разработки традиционных настольных приложений и видела в Интернете угрозу. На рис. 1.1 по ­ казано , как выглядел на то время стек технологий Micгosoft. ASP.NETWeb Forms Набор компонентов пользовательского интерфейса (страниц, кнопок и т.д.) плюс объектно-ориентированная программная модель графического пользовательского интерфейса с сохранением состояния ASP.NET Способ размещения приложений .NET в llS (веб-сервер Microsoft), позволяющий взаимодействовать с запросами и ответами НТТР . NET Мно гоязычная платформа управляемого кода (совершенно новая на то время и по праву ставшая поворотным пунктом в развитии) Рис . 1 . 1. Стек тех нолог ий ASP.NET Web Forms
Глава 1. Основы ASP.NET Core MVC 21 ASP .NET Web Forms В Web Forms разработчики из Microsoft пытались скрыть как протокол НТГР (с при­ сущим ему отсутствием состояния), так и язык HTML (которым на тот момент не вла­ дели многие разработчики) за счет моделирования пользовательского интерфейса как иерархии объектов , представляющих серверные элементы управления. Каждый элемент управления отслеживал собственное состояние между запросами, по мере необходимости визуализируя себя в виде НТМL-разметки и автоматически соединяя события клиентской стороны (например, щелчки на кнопках) с соответствующим ко­ дом их обработки на стороне сервера . Фактически Web Forms - это гигантский уровень абстракции, предн азначенный для доставки классического управляемого событиями графического пользовательского интерфейса через веб-среду. Идея заключалась в том, чтобы разработка веб-приложений выглядела почти так же. как разработка настольных приложений. Разработчики могли оперировать по­ нятиями пользовательского интерфейса, запоминающего состояние, и не иметь дело с последовательностями независимых запросов и ответов НТГР. У компании Microsoft появилась возможность переместить армию разработчиков настольных Windоws­ приложений в новый мир веб-приложений. Что было не так с ASP.NET Web Forms? Разработка с использ ованием традиционной технологии ASP.NET Web Forms в при­ нципе была хорошей, но реальность оказалась более сложной. • Тяжеловесност ь состояния представления (View State). Действительный меха­ низм поддержки состояния между запросами, известный как View State, вызвал необходимость передачи крупных блоков данных между клиентом и сервером. Объем таких данных мог достигать сотен килобайт даже в скромных веб-прило­ жениях. Эти данные путешествуют туда и обратно с каждым запросом, приводя к замедлению реакции и увеличивая требование к ширине полосы пропускания сервера. • ){{изненный 4икл страницы. Механизм соединения событий клиентской сто­ роны с ~юдом обработчиков событий на стороне сервера, являющийся частью жизненного цикла страницы, мог быть сложным и хрупким . Лишь немногим разработчикам удавалось успешно манипулировать иерархией элементов уп­ равления во время выполнения, не получая ошибки состояния представления или не сталкиваясь с ситуацией, когда некоторые обработчики событий зага­ дочным образом отказывались запускаться. • Ложное ощущение разделения обязанностей. Модель отделенного кода ASP.NET Web Forms предоставляла способ вынесения кода приложения из НТМL-разметки в специальный файл отделенного кода. Это было сделано для разделения логи­ ки и представления, но в действительности попустительствовало смешиванию кода представления (например, манипуляций деревом элементов управления серверной стороны) с прикладной логикой (скажем , обработкой информации из базы данных) в гигантских классах отделенного кода. Конечный результат мог быть хрупким и непонятным. • Ограниченный контроль над НТМL-разметкой. Серверные элементы управле­ ния визуализировали себя в виде НТМL-разметки, но она не обязательно была такой. как хотелось. В ранних версиях Web Forms выходная НТМL-разметка не
22 Часть 1. Введение в инфраструктуру ASP.NET Core MVC соответствовала веб-стандартам или неэффективно применяла каскадные таб­ лицы стилей (Cascading Style Sheets - CSS), а серверные элементы управления генерировали непредсказуемые и сложные атрибуты идентификаторов, к кото ­ рым было трудно получать доступ с помощью JavaScript. Эти проблемы были значительно смягчены в последних выпусках Web Forms, но получение ожидае­ мой НТМL- разметки по-прежнему могло быть затруднительным . • Негерметич.ная абстракция. Инфраструктура Web Forms пыталась скрывать де ­ тали, связанные с HTML и НТТР, где только возможно . При попытке реализовать специальное поведение абстракция часто отбрасывалась, поэтому для генериро­ вания желаемой НТМL-разметки приходилось воссоздавать механизм обратной отправки событий или предпринимать другие сложные действия. • Низкое удобство тестирования. Проектировщики Web Forms даже не пред­ полагали, что автоматизированное тестирование станет неотъемлемой час­ тью процесса разработки программного обеспечения . Построенная ими тесно связанная архитектура не была приспособлена для модульного тестирования. Интеграционное тестирование также могло превратиться в сложную задачу. С технологией Web Forms не все было плохо, и в Microsoft приложили немало уси­ лий по улучшению степени соответствия стандартам, упрощению процесса разра­ ботки и даже заимствованию ряда возможностей из первоначальной инфраструкту­ ры ASP.NET MVC Framework для их применения к Web Forms . Технология Web Forms превосходна, когда необходимы быстрые результаты, и с ее помощью вполне реально построить и запустить веб-приложение умеренной сложности всего за один день. Но если не проявлять осторожность во время разработки , то обнаружится, что созданное приложение трудно тестировать и сопровождать. Первоначальная инфраструктура MVC Framework В октябре 2007 года компания Microsoft объявила о выходе новой платформы раз­ работки, построенной на основе существующей платформы ASP.NET, которая была задумана как прямая реакция на критику Web Forms и популярность конкурирующих платформ наподобие Ruby on Rails. Новая платформа получила название ASP.NET МVС Framework и отражала формирующиеся тенденции в разработке веб - приложе­ ний, тшше как стандартизация HTML и CSS, веб - службы REST, эффективное модуль­ ное тестирование, а также идея о том, что разработчики должны принять факт от­ сутствия состояния у НТТР. Концепции, которые подкрепляли первоначальную инфраструктуру МVС Framework, теперь кажутся естественными и очевидными, но в 2007 году они отсутствовали в мире разработки веб-приложений .NET. Появление ASP.NET МVС Framework возврати­ ло платформу разрабопш веб-приложений Microsoft в современную эпоху. Инфраструктура МVС Framework также сигнализировала о важном изменении в отношении компании Microsoft, которая ранее пыталась контролировать каждый ком­ понент в инструментальных средствах, необходимых для веб-приложений . Компания Microsoft встроила в инфраструктуру MVC Framework инструменты с открытым ко­ дом, такие кaкjQuery, учла проектные соглашения и передовой опыт конкурирующих (и более успешных) платформ, а также выпустила в свет исходный код MVC Framework для изучения разработчиками.
Глава 1. Основы ASP.NET Core MVC 23 Что было не так с первоначальной инфраструктурой MVC Framework? На то время для Microsoft имело смысл создавать инфраструктуру МVС Framework повер х существующей платформы, содержащей большой объем монолитной низ­ коуровневой функциональности, которая обеспечила преимущество в начале про­ цесса разработки и которую хорошо знали и понимали разработчики приложений ASP.NET. Компромиссы потребовали основать MVC Framework на платформе, которая из­ начально предназначалась для Web Forms . Разработчики MVC Framework привыкли использовать конфигурационные параметры и кодовые настройки, отключающие или реконфигурирующие средства, которые не имели ни малейшего отношения к их веб­ приложениям , но требовались для того, чтобы все заработало. С ростом популярности MVC Framework компания Microsoft приступила к добав ­ лению ряда ее основных возможностей к Web Forms. Результат был все более и более странным, когда индивидуальные особенности проекта, требуемые для поддержки MVC Fra mework, расширялись с целью поддержки Web Forms, и предпринимались до­ полнительные проектные ухищрения, чтобы подогнать все друг к другу. Одновременно в Microsoft начали расширять ASP.NET новыми инфраструктурами для создания веб­ служб (Web АР!) и организации коммуникаций в реальном времени (SignalR) . Новые инфраструктуры добавили собственные соглашения по конфигурированию и разра­ ботке, каждое из которых обладало своими преимуществами и причудами, породив в результате фрагментированную смесь. Обзор ASP .NET Core В 20 15 году Microsoft заявила о новом направлении для ASP.NEТ и MVC Framewor k, которое в итоге привело к появлению инфраструктуры ASP.NET Core МVС, рассматри­ ваемой в настоящей книге. Платформа ASP.NET Core построена на основе .NET Core, которая представляет со ­ бой межплатформенную версию .NET Framework без интерфейсов программирования приложений (АР!), специфичных для Windows. Господствующей операционной сис­ темой по-прежнему является Windows, но веб - приложения все чаще размещаются в небольших и простых контейнерах на облачных платформах . За счет принятия меж­ платформенного подхода компания Microsoft расширила область охвата .NЕТ, сделав возможным развертывание приложений ASP.NET Core на более широком наборе сред размещения, а в качестве бонуса предоставила разработчикам возможность созда­ вать веб-приложения ASP.NET Core на машинах Linux и OS X/ macOS . ASP.NET Core - это сове р шенно новая инфраструктура. Она проще, с нею легче р а ботать, и она свободна от наследия, сопровождающего Web Forms. Будучи основан­ ной на .NET Core, она поддерживает разработку веб-приложений для ряда платформ и контейнеров. Инфраструктура ASP.NET Core МVС предоставляет функциональность первона­ чальной инфраструктуры ASP.NET MVC Framework, построенной поверх новой плат­ формы ASP.NET Core. Она включает функциональность, которая ранее предлагалась Web АР!, поддерживает более естественный способ генерирования сложного содержи­ мого и делает основные задачи разработки, такие как модульное тестирование, более простыми и предсказуемыми .
24 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Основные преимущества ASP.NET Core MVC В последующих разделах кратко описано, чем эта новая платформа MVC превос­ ходит унаследованную инфраструктуру Web Forms и первоначальную инфраструктуру MVC Framework, и что именно позволит вновь вывести ASP.NET на передний край. Архитектура MVC Инфраструктура ASP.NET Core MVC следует паттерну под названием "модель­ предсmавление-конmроллер" (model-view-controller - MVC), который управляет фор­ мой веб-приложения ASP.NET и взаимодействиями между содержащимися в нем компонентами. Важно различать архитектурный паттерн МVС и реализацию ASP.NEТ Core МVС. Паттерн МVС далеко не нов (его появление датируется 1978 годом и связано с про­ ектом Smalltalk в Xerox PARC), но в наши дни он завоевал популярность в качестве паттерна для веб-приложений по перечисленным ниже причинам. • Взаимодействие пользователя с приложением, которое придерживается паттер­ ном MVC, следует естественному циклу: пользователь предпринимает действие, а в ответ приложение изменяет свою модель данных и доставляет обновленное представление пользователю. Затем цикл повторяется. Это удобно укладывает­ ся в схему веб-приложений, предоставляемых в виде последовательностей за­ просов и ответов НТТР. • Веб-приложения, нуждающиеся в сочетании нескольких технологий (например, баз данных, НТМL-разметки и исполняемого кода), обычно разделяются на на­ бор слоев или уровней. Получ енные в результате таких сочетаний комбинации естественным образом отображаются на концепции в паттерне MVC. Инфраструктура ASP.NET Core МVС реализует паттерн МVС и при этом обеспечи­ вает гораздо лучшее разделение обязанностей по сравнению с Web Forms. На самом деле в ASP.NET Core MVC внедрена разновидность паттерна MVC, которая особенно хорошо подходит для веб-приложений. Дополнительные сведения по теории и прак­ тике применения этой архитектуры приведены в главе 3. Расширяемость Инфр аструктуры ASP.NET Core и ASP.NET Core МVС построены в виде последова­ тельности независимых компонентов, которые имеют четко определенные характе­ ристики, удовлетворяют интерфейсу .NET или соэданы на основе абстрактного базо­ вого класса. Основные компоненты можно легко заменять другими компонентами с собственной реализацией. В общем случае для каждого компонента инфраструктура ASP.NET Core MVC предлагает три воэможности. • Использование стандартной реализации компонента в том виде, как есть (чего должно быть достаточно для большинства приложений). • Создание подкласса стандартной реализации с целью корре1пировки существу­ ющего поведения. • Полная замена компонента новой реализацией интерфейса или абстрактного базового класса. Разнообразные компоненты, а также способы и причины их возможной настройки или замены будут рассматриваться, начиная с главы 14.
Глава 1. Основы ASP.NET Саге MVC 25 Жесткий контроль над HTML и НТТР Инфраструктура ASP.NET Core МVС генерирует ясную и соответствующую стан­ дартам разметку. Ее встроенные дескрипторные вспомогательные классы (tag helper) производят соответствующий стандартам вывод, но существует также гораздо более значимое философское изменение по сравнению с Web Forms. Вместо генерации гро­ мадного объема НТМL-разметки, над которой вы имеете очень небольшой контроль, инфраструктура ASP.NET Core MVC поощряет создание простой и элегантной размет­ ки. стилизованной с помощью CSS. Конечно, если вы действительно хотите добавить готовые виджеты для таких сложных элементов пользовательского интерфейса, как окна выбора даты или кас­ кадные меню , то подход "никаких специальных требований", принятый в ASP.NET Core МVС, позволяет легко использовать наилучшие клиентские библиотеки. подоб­ ные jQuery, Aпgular или Bootstrap CSS. Инфраструктура ASP.NET Core MVC настолько тесно сплетена с этими библиотеками, что компания Microsoft включила их подде­ ржку как встроенных частей стандартного шаблона проектов для веб-приложений. Инфраструктура ASP.NET Core МVС работает в гармонии с НТТР. Вы имеете кон­ троль над запросами, передаваемыми между браузером и сервером, так что можете точно настраивать пользовательский интерфейс по своему усмотрению. Технология Ajax является легкой в применении, и создание веб-служб для получения браузерных НТТР-запросов представляют собой простой процесс, описанный в главе 20. Тестируемость Естественное разнесение различных обязанностей приложения по независимым друг от друга частям, которое поддерживается архитектурой ASP.NET Core MVC, поз­ воляет с самого начала делать приложение легко сопровождаемым и удобным для тестирования. Вдобавок каждый фрагмент платформы ASP.NET Core и инфраструк­ туры ASP.NET Core МVС может быть изолирован и заменен в целях модульного тес­ тирования , которое допускается выполнять с использованием любой популярной ин­ фраструктуры тестирования с оп,рытым кодом, такой кан xUnit, рассматриваемой в главе 7. В этой книге вы увидите примеры написания ясных и простых модульных тестов для контроллеров и действий MVC, которые предоставляют фиктивные либо имити­ рованные реализации компонентов инфраструктуры для эмуляции любого сценария с применением разнообразных стратегий тестирования и имитации. Даже если вам никогда ранее не приходилось создавать модульные тесты, у вас будет все необходи­ мое для успешного старта. Тестируемость касается не только модульного тестирования . Приложения ASP.NET Core МVС успешно работают также с инструментами тестирования, встроенными в средства автоматизации пользовательского интерфейса. Можно создавать тестовые сценарии, которые имитируют взаимодействие с пользователем, не выдвигая догадки о том , какие структуры НТМL-элементов, классы CSS или идентификаторы сгенери­ рует инфраструктура, и не беспокоясь о неожиданных изменениях структуры. Мощная система маршрутизации По мере совершенствования технологии построения веб-приложений эволюциони­ ровал стиль унифицированных указателей ресурсов (uniform resource locator - URL).
26 Часть 1. Введение в инфраструктуру AS P.NE T Core MVC Адреса URL, подобные приведенному ниже: /App_v2/U ser/Page . as p x?act ion = show%2 0p rop&prop id= 8 2742 встречаются все реже, а на смену им приходит более простой и понятный формат следующего вида: / t o -rent/chic a g o/2303 - sil v er -street Существует ряд веских причин для того, чтобы заботиться о структуре URL- aдpecoв . Во-первых, поисковые механизмы придают вес ключевым словам, содержа­ щимся в URL. Поиск по словосочетанию "rent in Chicago" (аренда в Чикаго) с большей вероятностью обнаружит более простой URL. Во-вторых, многие веб-пользователи достаточно сообразительны, чтобы понять URL, и ценят возможность навигации путем ввода запроса в адресной строке своего браузера. В-третьих, когда структура URL-aдpeca понятна. люди с большей вероятностью пройдут по нему, поделятся им с другими или даже продиктуют его по телефону . В-четвертых, при таком подходе в Интернете не раскрываются технические детали, структура каталогов и имен файлов приложения; следовательно, вы вольны изменять лежащую в основе сайта реализа­ цию, не нарушая работоспособности всех входящих ссьшок . В более ранних инфраструктурах понятные URL- aдpeca реализовать было трудно, но в ASP.NET Core MVC используется средство, известное как маршрутизация URL, которое обеспечивает предоставление понятных URL- aдpecoв по умолчанию. Это дает контроль над схемой URL и ее взаимосвязью с приложением, обеспечивая свободу создания понятного и удобного для пользователей шаблона URL без необходимости следования какому-то заранее определенному шаблону. И, разумеется. это означает тан:же простоту определения современной схемы URL в стиле REST, если она нужна. Подробное описание маршрутизации URL приведено в главах 15 и 16. Современный АРl-интерфейс Платформа Microsoft .NET развивалась с каждым крупным выпуском, поддержи­ вая - и даже определяя - многие передовые аспекты современного программирова­ ния . Инфраструктура ASP.NEТ Core MVC построена для платформы .NET Core, поэто­ му ее АРI-интерфейс может в полной мере задействовать последние новшества языка и исполняющей среды. знакомые программистам на С# , в том числе ключевое слово aw a i t, расширяющие методы, лямбда-выражения, анонимные и динамические типы, а также язык интегрированных запросов (Language Integrated Query - LINQ) . Многие методы и паттерны кодирования АРI-интерфейса ASP.NET Core MVC сле­ дуют более четкой и выразительной композиции, чем это было возможно в ранних версиях инфраструктуры . Не переживайте, если вы пока не в курсе последних функ­ циональных возможностей языка С#: в главе 4 предоставлена сводка по самым важ­ ным средствам С# для разработки приложений МVС . Межплатформенная природа Предшествующие версии ASP.NET были специфичными для Windows, требуя на­ стольный компьютер с Windows для написания веб-приложений и сервер Windows для их развертывания и выполнения . Компания Microsoft сделала инфраструктуру ASP.NEТ Core межплатформенной, как в отношении разработки, так и в плане раз­ вертывания . Продукт .NET Core доступен для различных платформ , включая Linux и OS X/ macOS. и вероятно будет переноситься на другие платформы.
Глава 1. Основы ASP.NET Саге MVC 27 Большая часть разработки приложений ASP.NET Core MVC в ближайшем будущем, скорее всего, будет выполняться с применением Visual Studio, но компания Microsoft также создала межплатформенный инструмент разработки под названием Visual Studio Code , появление которого означает, что разработка ASP.NEТ Core MVC больше не ограничивается Windows. Инфраструктура ASP.NET Core MVC имеет открытый код В отличие от предшествующих платформ для разработки веб-приложений от Microsoft вы можете загрузить исходный код ASP.NEТ Core и ASP.NET Core МVС и даже модифицировать и компилировать его с целью получения собственных версий инфра­ структур. Это бесценно при отладке кода, обращающегося 1< системному компоненту, когда требуется пошагово вьmолнить его код (и даже почитать исходные комментарии программистов). Это также полезно, если вы создаете усовершенствованный компо­ нент и хотите посмотреть, какие существуют возможности разработки, или узнать, как действительно работают встроенные компоненты. Исходный код ASP.NET Core и ASP.NET Core МVС доступен для загрузки по адресу: https://github .com/aspnet Что необходимо знать? Чтобы извлечь максимум из этой книги, вы должны быть знакомы с основами раз­ работки веб-приложений, понимать HTML и CSS, а также иметь практический опыт работы с языком С#. Не беспокойтесь, если детали разработки клиентской стороны, такие как JavaScript. для вас несколько туманны. Основной акцент в книге делается на разработке серверной стороны. и благодаря примерам вы сможете подобрать то, что вам нужно. В главе 4 приводится сводка по наиболее полезным средствам языка С# для разработки MVC, которую вы сочтете удобной, если переходите на последние версии .NET с более раннего выпуска. Какова структура книги? Эта 1\нига разделена на две части, в каждой из которых раскрывается набор свя­ занных тем. Часть 1. Введение в инфраструктуру ASP.NET Core MVC Книга начинается с помещения ASP.NET Core MVC в контекст разработки. Здесь объяснены преимущества и практическое влияние паттерна МVС, рассмотрен способ, которым инфраструктура ASP.NET Core MVC вписывается в современную разработку веб-приложений, а также описаны инструменты и средства языка С#, необходимые каждому программисту ASP.NEТ Core МVС. В главе 2 мы углубимся в детали и создадим простое веб-приложение, чтобы по­ лучить представление о том, каковы основные компоненты и строительные блоки, и каким образом они сочетаются друг с другом. Однако большинство материала этой части посвящено разработке проекта под названием SportsStore, посредством которо­ го демонстрируется реалистичный процесс разработки, начиная с постановки задачи и заканчивая развертыванием, с привлечением основных функциональных возмож­ ностей ASP. NEТ Core MVC.
28 Часть/ . Введение в инфраструктуру ASP.NET Саге MVC Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Во второй части объясняется внутренняя работа средств ASP.NET Core MVC, ко­ торые использовались при построении приложения SportsStore. Будет показано, как работает каждое средство, объяснена его роль и описаны доступные варианты конфи­ гурирования и настройки. Широкий контекст, представленный в первой части, под­ робно раскрывается во второй части. Что нового в этом издании? Настоящее издание пересмотрено и расширено с целью описания инфраструкту­ ры ASP.NET Core МVС, которая отражает полную смену способа поддержки разработ­ ки веб-приложений компанией Microsoft. Ранние версии МVС Framework были пост­ роены на основе платформы ASP.NET, которая первоначально создавалась для Web Forms. Это обеспечило преимушество предоставления зрелого фундамента для раз­ работки MVC, но такими путями, которые допустили просачивание деталей работы Web Forms. Одни средства открывали доступ к внутренним особенностям Web Forms. не имеющим отношения к приложениям MVC, а другие могли порождать непредска­ зуемые результаты. Кроме того, фундамент ASP.NET предоставлялся с применением сборок, которые были включены в .NET Framework, что означало возможность внесения крупных из­ менений только при выпуске новой версии .NЕТ. Это стало проблемой, поскольку темп изменения разработки веб-приложений превосходил частоту изменения .NET . Инфраструктура ASP.NET Core MVC полностью переписана с сохранением филосо­ фии и общего проектного решения, заложенного в ранних версиях, но с обновлени­ ем АРI-интерфейса для улучшения дизайна и производительности веб-приложений. Инфраструктура ASP.NET Core МVС зависит от платформы ASP.NET Core, которая сама является полной переделкой базового стека веб-технологий: главенствующая роль Web Forms исчезла, а сильная связь с выпусками .NET Framework была разорвана. Вы можете счесть степень изменений тревожной, если имеете опыт работы с МVС 5, но не паникуйте . Лежащие в основе 1юнцепции остались теми же самыми, а многие изменения только выглядят более значительными и сложными, чем есть на самом деле. Во второй части книги приведена сводка по изменениям для каждого крупного средства, которая облегчит переход с МVС 5 на ASP.NET Core MVC. Где можно получить код примеров? Все примеры, рассмотренные в книге, доступны для загрузки на веб-сайте из­ дательства. Загружаемый файл включает все проекты вместе с их содержимым . Загружать код необязательно, но это самый простой путь для экспериментирования с примерами, а также использования фрагментов кода в собственных проектах. Резюме В этой главе был объяснен контекст, в котором существует инфраструктура ASP.NET Core MVC, и описано ее развитие от Web Forms и первоначальной инфраструктуры ASP.NET MVC Framework. Кроме того, рассматривались преимущества применения ASP.NEТ Core МVС и структура этой книги. В следующей главе вы увидите ASP.NET Core MVC в действии, благодаря простой демонстрации средств, которые проявляют все ее пр еимущества.
ГЛАВА 2 Ваше первое приложение МVС л учший способ оценки инфрастру1<туры, предназначенной для разработки про­ граммного обеспечения, заключается в том, чтобы приступить непосредственно к ее использованию. В этой главе мы создадим простое приложение ввода данных с применением ASP.NET Core MVC. Мы будем решать эту задачу пошагово, чтобы вы поняли, каким образом строится приложение MVC. Для простоты мы пока опустим некоторые технические подробности . Но не беспокойтесь - если вы только начина­ ете знакомство с MVC, то узнаете много интересного. Когда что-либо используется без пояснения, то приводится ссылка на главу, в которой находятся все необходимые детали . Установка Visual Studio Настоящая книга опирается на среду Visual Studio 2015, ноторая предлагает все, что понадобится для разработки ASP.NET Core МVС . Мы будем применять бесплат­ ную редакцию Visual Studio 2015 Community, доступную для загрузки на веб-сайте www . v is ua l st u dio . сот . При установке Visual Studio вы должны удостовериться в том , что отмечен флажок Microsoft Web Developer Tools (Инструменты разработчика веб-приложений от Microsoft). Совет. Среда Visual Studio поддерживает только Windows . Создавать приложения ASP.NET Core MVC можн о и на других платформах, используя продукт Visual Studio Code, но он не предоставляет все инструменты, требуемые для выполнения примеров в этой книге. За подробностями обращайтесь в главу 13. При наличии существующей установленной копии Visual Studio вы должны при­ менить обновл ение Visual Studio Update 3, которое предоставляет поддержку для ра­ боты с приложениями ASP.NET Core. В новых установках Visual Studio это обновление применяется автоматически. Оно доступно для загрузки по адресу; ht t p: //go . micr osoft . com/fwl ink/?Linkid=691129 Далее потребуется загрузить и установить платформу .NET Core, устано­ вочный файл которой находится по адресу h ttps : //go . mic r osoft .c o m / fwli n k /? Link i d = Bl 72 45 . Загружаемый установочный файл .NE T Core обязателен даже для новых установок Visual Studio.
30 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Финальный шаг предусматривает установку инструмента под названием g i t , до­ ступного для загрузки по адресу https :/ / git -scm. c om/ downl oad. Продукт Visual Studio включает собственную версию g i t , но она не работает должным образом и приводит к получению непредсказуемых результатов, когда используется другими инструментами. в том числе Bower, который будет описан в главе 6. Во время уста­ новки g i t удостоверьтесь, что указываете программе установки на необходимость до­ бавления пути к инструменту в перем енную среды РАТН (рис. 2 . 1). Это позволит ср еде Visual Studio найти новую версию g i t . ~~~-------------~-----------~ ...., Git2.9.0 Setup Adjustin9 your РАТН t!nvironme nt Но•и \\'OIJd you likr to ~ Glt IТom the cOl'f'VМnd line? ------- ------------------- ОUR Glt from GitSash onty 1hlsls lhe sofost choke о. VO<X РАПi ~il notЬe moditied ot <>I. You"111 onlyЬe .ы. tD., .. lhe Git СО111М11d line tools rтom Git В."1. @ Use Gitfrom the Windows Con1m• nd Pro mpt ТNo optfon is ~s.sfe asltonlyadds some minirмl Gitwr~ to YQU" РАТН to avold ciJttering YotX ~t ·дith optiornlf t.ni.X tools . You wil Ье .Ы. tD use Git fтom Ьoth Git в..t1 ш! lhe 1'Г111dом Coomand Prompt . О Use Git ;)hd optiona l UnЬc tools from th e Windows Comm and Pro n1pt Вoth Git .00 lhe optlonal Ur<x tools ~il Ье odded to VO<X РАПi. \Yaming: Th1s w\11 oVl!rrlde Wim!ows tooJs /Jke "fmd" and "юrt•. Only use tl>ls optIOD ifVoU Wld<!t'Stand the impical!on5. C.::_~_JI Next > 1r~~-::J Рис. 2.1 . Добавление пути к g i t в переменную среды РАТН Запустите Visual Studio, выберите в меню Tools (Сервис) пункт Options (Параметры) и перейдите в раздел Projects and SolutionsqExternal Web Tools (Проекты и решения<=> Внешние веб-инструменты), как показано на рис. 2 .2 . Снимите отметку с флажка $ (VS I NSTA LLD I R) \We b \External \ gi t , чтобы блокировать з апуск встроенной в Visual Studio версии g it, и проверьте , отмечен ли флажок$ (РА Т Н ), чтобы мог при­ м еняться только что установленный инструмент g i t . ! SeatckOpьc.-is(Ctrf•E) .Р lootions of cttrn<tl toot~: ~@;JwG 1 ~?~:~c·:~ons 1 :, 1 :B~~u:~.".~l.d;•:in~dR:~un:;:ng'iА Prci)tcts • nd Solutions G1:nu1I .NЕТ Соц~ P1oje<ts 1 !>tt<Nf\'/ф 1ook' 1 VSOtf;щ lts VC•• Oircclorit.~ VC• ~ P1oj«t Sdtings ,_ •...::.~ P< Oj « ~ L _________ - - - -------, 1 &') S(РАТН] ! 0 S(VSJNSTдt l DIR)\Wt b\Exte 1мf\g il 1 1 i 1 1 1! i 1 '·------------------------........) ! Re5tttoDl!f.щtts j The p•th~ listed obtwewШ Ье s.t11rched when V'кu.гl Studio U1U 3rd·(nrtytcols such IJ G1un~ Gulp, 8owtr, cr npm. -···- -~· -·-~· ---·- ... ------ -· ·- ·- ------- .. -~· ··-··- ~-о_к_~I Г~~~ _J ; Рис. 2.2 . Конфигурирование запуска g i t в Visual Studio
Глава 2. Ваше первое приложение MVC 31 Будущее ASP.NET Core MVC и Visual Studio В Microsoft недооценили, сколько времени потребовалось для создания ASP.NET Core и ASP.NET Core MVC. Первоначально запланированные даты выпусков совпали с выходом Visual Studio 2015, но задержки со стороны ASP.NET означают, что когда писались эти строки, разработка следую­ щей версии Visual Studio уже началась. Инструментальная поддержка для создания приложений ASP.NET Core MVC может измениться , когда будет выпущена следующая версия Visual Studio . После того как инструменты стабилизируются, я предоставлю обновление с инструкциями, требующими­ ся для создания примеров приложений. Новости будут доступны на веб-сайте издательства. Создание нового проекта ASP.NET Core MVC Мы собираемся начать с создания нового проекта ASP.NET Core МVС в среде Visual Studio. Выберите в меню File (Файл) пункт NewqProject (СоздатьQ Проект), чтобы открыть диалоговое окно New Project (Новый проект). Перейдя в раздел Templateso::>Visual С#<=:> Web (ШaблoныQVisual С#QВеб) в панели слева, вы увидите шаблон проекта ASP.NET Core Web Applicatioп (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Выберите этот тип проекта (рис. 2.3). 1 t> Rtcent i• lnstall<d ! • Template~ ... Visual(# 1 f> Online Name: w.ь .NПCore Android Cloud bll!ns!Ьil1ty .os Reporting S1fvttlight l locatio n: So lut iori ntn,,e: P artylnvitм P.-trty!nvite. ~ ~"!_ЕТ Fr"m~;~ :б~1 - _: . Sort Ьу: \Dtf-;~ij_--_-- -·-~. ") i~ ~ :::: @j A5P.NПWe.bApp1icгtion{.NE1 fr~me.work) VisualC• @ A.SP.NП Core Wtb Applit•tion (.NП fr"mework) Vт~ua/ с~ Click h(r~ 10 по onl•"( ond f1t'!d 1wRIJ.tg. ~мchi~7~tit!d "ТtmplalF:j_Ctrl... E) ~~-!: .- ; Туре: Visua l (# Project ttmplat6 for crtating ASP.N П Core "pplic<1tio ns forWindows, l inux and OS Х щing .NE1 Core. ' AppfКAtion l nslgh ts О Add Appf1c<1t1on lnsights to projкt Optimi:tt ptrformt!11Ct 4nd monitor us<1Qt in vour li<.•e "PPlit"ti on. Help me unde.r~t""d Applka tio n lns19hts P11\'iicySt<11ll!.merit ВloWft". 1 0 Cre.111e. dlltctory fo r.solutiol'\ О Add to Source Contro1 l-------~~,~=~~ ,~~~~~Od-L Сап~ Рис. 2.3 . Шаблон проекта ASP.NET Core Web Applicatioп (.NET Core) в Visual Studio Совет. При выборе шаблона проекта может возникать путаница из-за сильно похожих на­ званий шаблонов. Шаблон ASP.NET Web Applicatioп (.NET Framework) (Веб-приложение ASP.NET (.NET Framework)) предназначен для создания проектов с использованием унас­ ледованных версий ASP.NET и MVC Framework, которые предшествовали ASP.NET Core . Остальные два шаблона позволяют создавать приложения ASP.NET Core и отличаются применяемой исполняющей средой, предлагая на выбор .NЕТ Framework или .NET Core. Разница между ними объясняется в главе 6, но повсеместно в книге используется вари­ ант .NET Core, поэтому для получения идентичных результатов при работе с примерами приложений вы должны выбирать именно его.
32 Часть 1. Введение в инфраструктуру ASP.NET Core MVC В пол е Name (Имя) для нового проекта введите P a r ty i nv i tes и удостоверь­ тесь в том , что флажок Add Application lnsights to Project (Добавить в проект службу Application Insights) не отмечен (см . рис. 2.3). Для продолжения щелкните на кнопке ОК. Откроется еще одно диалоговое окно (рис. 2.4), предлагающее установить началь­ ное содержимое проекта . г-·-·--·------·-----·------· · ·-1 1 iMP; CoreTcrn ; 1 . En1 pty ltleb API 1 А proje ct template for crto ting an ASP.NEТ Соге app lication with exll mpfe ASP.NEТ MVC Vi~s and Cont fo!lers. This temp late сап ~lso Ье used for RESТfuf НТТР service:. · 1 · 11 1 J @haпgt'AUt~ 1{ _ Authentication: No Au thentlc~tion ---- -1 1 ------· - ·--·- 1<;;, MicrcкoftAzu re 1 \G О Host iп thecloud j 1 ~1~.,.-г:з uo ~ I L ~ Рис. 2.4 . Выбор начальной конфигурации проекта Для шаблона ASP.NET Core Web Application (.NET Core) доступны три вариант а , каждый из которых приводит к созданию проекта с отличающимся начальным содер­ жимым. Для целей данной главы выберите вариант Web Application (Веб-приложение), который настроит приложение MVC с заранее определенным содержимым , чтобы не­ медленно приступить к разработке. На заметку! Это единственная глава, в которой применяется шаблон проекта Web Application. Мне не нравится пользоваться заранее определенными шаблонами , поскольку они пот­ ворствуют интерпретации ряда важных средств наподобие аутентификации как черны х ящиков . Моя цель в настоящей книге - предоставить вам достаточный объем знаний для понимания и управления каждым аспектом приложения MVC, так что в оставшихся мате­ риалах книги применяется шаблон Empty (Пустой). Текущая глава посвящена быстрому началу процесса разработки , для чего хорошо подходит шаблон Web Application. Щелкните на кнопке Change Authentication (Изменить аутентификацию) и проверь­ те, что выбран переключатель No Authentication (Аутентификация отсутствует), 1<ак показано на рис. 2.5 . Данный проект не требует какой-либо аутентификации, а в гла­ вах 28-30 объясняется , как защитить приложения ASP. NEТ. Щелкните на кнопке ОК, чтобы закрыть диалоговое окно Change Authentication (Изменение аутентификации). Удостоверьтесь в том , что флажок Host in the Cloud (Разместить в облаке) не отмечен и затем щелкните на кнопке ОК для создания про­ екта Par t yi n v i t e s. После того как среда Visual Studio создала проект, вы увидите в окне Solution Explorer (Проводник решения) множество файлов и папок (рис . 2.6). Это стандартная структура для проекта МVС, созданного с использованием шаблона Web Application , и вскоре вы узнаете на з начение каждого файла и папки , которые были созданы Visual Studio.
Глава 2. Ваше первое приложение MVC 33 For applic•tions that don 't requi re any use r • uthentic• tio". @ No Auth•ntication О lndividual User Accounts О Work And Scl>ool Accou nt s О Windows Authentication г-о~_:] \ Cьncel Рис. 2.5. Выбор настроек аутентификации Теперь можете запустить приложение, выбрав в меню Debug (Отладка) пункт Start Debugging (Запустить отладку); если появит- ся запрос на включение отладки, тогда просто щелкните на кнопке ОК. Среда Visual Studio скомпилирует приложение, с помощью сервера приложений IIS Express запусти т его и откро­ ет окно веб-браузера для запроса содержимого приложения. Результат показан на рис. 2.7. Когда среда Visual Studio создает проект с при­ менением шаблона Web Application, она добавля­ ет базовый код и содержимое, которое вы видите после запуска приложения. В оставшейся части этой главы мы будем заменять такое содержимое, чтобы создать простое приложение МVС. По завершении не забудьте остановить от­ ладку, закрыв оюю браузера или вернувшись в Visual Studio и выбрав в меню Debug пункт Stop Debugging (Остановить отлад~<у). Как было только что показано, для отображе­ ния проекта среда Visual Studio открывает окно браузера. Вы можете выбрать любой установ­ ленный браузер , щелкнув на кнопке со стрелкой Sol uti on Explorer • 1:1Х .~ с' ~ f''"Ф •·4'~·@ Wi 1,,, -- . ..:.,;;,,,·"- ··' ·' .,;:;;;i~.-:. .~: \о...., •·.1, - ._, .se~5~~l u~on ~plorer (Ctrt,:;~ l4J Solution 'Partylnvites' ( 1 project} " ;,,. , . Solution Jtem s /;J global.json ..; ·~1 src ~ ,1- Properties ~ •·• Referenc e.s •·• Depe.ndencies ~ ;.t; Cont roll ers ~ ~ Vieo.vs lJ appsettings.json lJ bundleconfigjso n С" Program.cs ~ lJ project.json .D Project_Readme.html с• Startup.cs у') web. co nfig Р· Рис. 2.6. Начальная структура файлов и папок проекта ASP.NET Саге MVC правее кнопки llS Express в панели инструментов и выбрав нужный вариант из спис­ ка в меню Web Browser (Веб-браузер), как показано на рис. 2.8. В дальнейшем во всех примерах в книге будет использоваться браузер Google Chrome или Google Chrome Canary, но вы можете применять любой современный бра­ узер, включая Microsoft Edge и последние версии Internet Explorer. Добавление первого контроллера В рамках паттерна MVC входящие запросы обрабатываются контроллерами. В ASP.NET Core MVC контроллеры - это просто классы С# (обычно унаследованные от класса Microsoft. AspNetCore . Mvc. Controlle r, являющегося встроенным ба­ зовым классом контроллера МVС).
34 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Appl ication uses How to Overvi ew Run & Dep loy • ~;а 1>о191-? UWЧ ASl'I Nfl CCrtMVC • &.-l!l<m.Ul,}f)IП',j(~.$4dil ...... • T h fm!ng 11~ И«щщр • AddaQ.imrt'Jl!<'or....Ю'/- ' A'1':-."'lapps.;it!l>l)lflщ\/.glil'l<I ......,.."lln~ · M~-...Uwl~ll"l 'lf.l'f.l>I !.l1~ · \.lw~'!/.IU~·~ • -'4. Jpat kog' !'>u Plg tll>O ~ • A(l('ltl<lrllpN~u""'JI~ o ljjf\l('f~fl''"'l.ЦloJ ""JI)( ptOd!,ГЬIК\Wtlr.'JIИ'I( • Глrщ.Ш:.lt.>."4,.,.......iilwl~•t" ASPtlП ~ • Futnr.n1>00!ЬolNil 'Nf J C:.,." ~,,St.!1rup.,11::!~ • .,. · ~-iti~ ·~ 0 (.l~t-"'~•wr.t • ri.,.y,4<\p(">(j..!l<>:nr~f'l>,1'.IO'.~ • r~.t dn"3 f"\ ,"!' l'"° (!t, .'Q ,l/N >~"' tл1 · 11-"'У"'" '""" • Rllft l(ll'll•&ur. ~.\ ~fТtr~ /INl"ll'.r" • l'ld-Jh •<J !~/: .w.n.ww '"" '1l0 1t!l · l'iwfytпrrtн Test. Рис. 2. 7 . Выролнение примера проекта An•lyz• Wi ndow H•lp .-'------->=~. • 1 ~..., L__·_ ......... ll SExp ress llS Express Partyf nvit•s Web Browш (G oog l• Cl11ome) Google Chro me Google Chrome C~n~ry ~ lntern et Exp lorer Microsoft Edge Рис. 2.8. Выбор браузера Каждый открытый метод в контроллере называется методом действия, что оз­ начает возможность его вызова из веб-среды через некоторый URL для выполнения действия. В соответствии с соглашением MVC контроллеры помещаются в папку Controllers , автоматически создаваемую Visual Studio при настрой1<е проекта. Совет. Вы вовсе не обязаны соблюдать указанное или большинство других соглашений MVC, но рекомендуется его придерживаться - и не в последнюю очередь потому, что это по­ может уяснить примеры , приведенные в настоящей книге . Среда VisuaJ Studio добавляет в проект класс стандартного контроллера, который вы можете увидеть , раскрыв папку Control le r s в окне Solution Explorer. Файл назы­ вается Ho meCont r ol l e r. cs . Файлы классов контроллеров имеют имя, завершающе-
Глава 2. Ваше nервое nриложение MVC 35 еся словом Control ler, т. е. в файле Home Controll e r. c s содержится код контролле­ ра по имени Home - стандартного контроллера. используемого в приложениях МVС. Щелкните на имени файла Ho meCont r o ll er . cs в окне Solution Explorer, чтобы среда Visual Studio открыла е го для редактирования. Вы увидите код С# , приведенный в ли стинге 2.1. Листинг 2.1 . Первоначальное содержимое файла HomeController. cs из папки Controllers using System ; using System.Col lections.Gene ric; using System . Li nq; using S ystem . Thread ing . Tasks ; using Microsoft . AspNe t Core . Mvc ; namespace Pa r tyinvites .C ontrol l e r s puЫic class HomeCon tr olle r : Co ntr o ller puЫic IActionRes ult I ndex() { return View () ; puЫic IAct i onResult About() { ViewData ["Message " ] = " You r appl ication description pag e ."; return View() ; puЫic IActionRes ul t Contact ( ) { ViewData [ "Message"] = " Your c ont act page . "; return View() ; puЫic IActionResu lt Error() { return Vi ew() ; Замените код в файле HomeContro l ler . c s кодом, показанным в листинг е 2.2 . Эде сь были удалены все методы кроме одного , у которого изменен возвращаемый тип и его р еали з ация, а также удалены операторы u s i ng для неиспользуемых про ­ странств имен. Листинг 2.2 . Изменение файла HomeController. cs using Microsof t. AspNetCore .M vc ; n amespace Partyi nvi te s.Cont r o ller s puЫic class HomeControl ler : Cont r oller puЫic stri ng Ind ex() { return "Hello World";
36 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Изменения не приводят к особо впечатляющим результатам, но их вполне доста­ точно для хорошей демонстрации. Метод по имени I n dex () изменен так. что теперь он возвращает строку "H e llo Wor l d ". Снова запустите проект, выбрав пункт Start Debuggiпg из меню Debug в Visual Studio. Совет. Если вы оставили в функционирующем состоянии приложение из предыдущего разде­ ла , то выберите в меню Debug пункт Restart (Перезапустить) или при желании пункт Stop Debuggiпg и затем Start Debuggiпg. Браузер сделает НТТР-запрос серверу. Стандартная конфиrурация MVC предусмат ­ ривает, что данный запрос будет обрабатываться с применением метода Index () , на­ зываемого методом действия или просто действием, а результат, полученный из этого метода , будет отправлен обратно браузеру (рис. 2 .9). 1 l0<a!host:S7628 Х lo World 1 l: _ _?_@l~c~~~~~~~-8- ·~-=-=--~=-=---~-=--= _~-j _ -----------·------- __, Рис. 2.9 . Вывод из метода действия Совет. Обратите внимание, что среда Visual Studio направляет браузер на порт 57628. Внутри URL, который будет запрашивать ваш браузер, почти наверняка будет присутствовать другой номер порта, т. к. Visual Studio выделяет произвольный порт при создании проекта . Если вы заглянете в область уведомлений панели задач Windows, то найдете там значок для llS Express. Этот значок представляет усеченную версию полного сервера приложе­ ний llS, которая входит в состав Visual Studio и используется для доставки содержимого и служб ASP.NET во время разработки. Развертывание проекта MVC в производственной среде будет описано в главе 12 . Понятие маршруто в В дополнение к моделям , представлениям и контроллерам в приложениях MVC применяется система маршрутизации ASP.NEТ, которая определяет, как URL отоб­ ражаются на контроллеры и действия. Маршрут - это правило, которо е использу­ ется для решения о том, как обрабатывать запрос. Когда среда Visual Studio созда ет проект MVC, она добавляет ряд стандартных маршрутов , выступающих в качеств е начальных. Можно запрашивать любой из следующих URL, и они будут направлены на действие Index класса H oтeC ont r o lle r : ./ • /Ноте • / Hoтe/Index Таким образом, когда брауз ер запрашивает http : //ваш- сайт/ или http: //ваш_ сайт/ Н оте , он получает вывод из метода I ndex () класса H oтeController. Можете
Глава 2. Ваше первое приложение MVC 37 опробовать зто самостоятельно, изменив URL в браузере. В настоящий момент он бу­ дет выглядеть как http: //localhost : 57628/, но представляющая порт часть мо­ жет быть другой . Если вы добавите к URL порцию /Home или /Home/Index и нажмете клавишу <Enter>. то получите от приложения MVC тот же самый результат - строку "Hello, world". Это хороший пример получения выгоды от соблюдения соглашений, поддержива­ емых ASP.NET Core MVC. В данном случае соглашение заключается в том, что имеет­ ся контроллер по имени HomeController. который будет служить стартовой точкой приложения MVC. Стандартная конфигурация. 1<0торую V!sual Stud!o создает для но­ вого проекта, предполагает. что мы будем следовать этому соглашению. И поскольку мы действительно соблюдаем соглашение, мы автоматически получаем поддержку всех URL из приведенного выше списка. Если не следовать соглашению, то конфигу­ рацию пришлось бы модифицировать для указания на контроллер, созданный вза­ мен стандартного. В рассматриваемом простом примере стандартной конфигурации вполне достаточно. Визуализация веб-страниц Выводом предыдущего примера была не НТМL-разметка. а просто строка "Hel lo Wor ld ". Чтобы сгенерировать НТМL-ответ на запрос браузера, понадобится создать представление, которое сообщает MVC. каким образом генерировать ответ для запро­ са, поступившего из браузера. Создание и визуализация представления Прежде всего, необходимо модифицировать метод действия Index () , как показа­ но в листинге 2 .3 . Для простоты восприятия в этом и во всех будущих листингах из­ менения выделяются полужирным. Листинг 2.3. Изменение контроллера для визуализации представления в файле HomeController.cs using Microsoft . AspNetCore . Mvc; namespace Partyinvites.Controllers puЫic class HomeController : Controller puЬlic ViewResult Index () return View("MyView"); Возвращая из метода действия объект ViewResu l t, мы инструктируем МVС о ви­ зуализации представления. Экземпляр ViewResul t создается посредством вызова метода View () с указанием имени представления. которое должно применяться, т.е. MyView. Запустив приложение, можно заметить, что инфраструктура MVC пытается найти представление, кш< отражено в сообщении об ошибке на рис. 2 . 10. Сообщение об ошибке исключительно полезно. Оно не только объясняет, что ин­ фраструктура MVC не смогла найти представление, указанное для метода действия, но также показывает, где производился поиск. Представления хранятся в подпапках внутри папки Views .
38 Часть 1. Введение в инфраструктуру ASP.NET Core MVC lrite-rnal Serv~ Erro r Х :1 ---- --··· •1 1 An Linhandled exception оссшгеd while pro cessing the reqL1est . f lnvaliclOpeгatio'lExcep<ion: Т11е viev1 'MyViev/ v1as 110t fou r. d . Th e fo i/01·1ing !ocat ions were searc f1 ed : . /Views/Home/MyView.cshtinl i Niews/Si1ared/MyVie\\1.CSl-1t •nl f Eпiun:-su,cessful 1 В Q\Jery Cookies Heиders 1.- ···---- - . Рис . 2.1 О. Инфраструктура MVC пытается найти представление ii1 i 1 :i Например, представления, которые связаны с контроллером Home , содержатся в папке по имени V i ew s / Ho me . Представления, не являющиеся специфическими для отдельного контроллера, хранятся в папке под названием V ie ws / Sha r ed . Среда Visual Studio создает папки Ho me и Sh a red автоматически, когда используется шаб­ лон Web Application, и в рамках начальной подготовки проекта помещает в них не­ сколько представлений-заполнителей. Чтобы создать представление , щелкните правой кнопкой мыши на папке Home внутри V i ews в окне Solution Explorer и выберите в контекстном меню пункт Add~New ltem (Добавить~Новый элемент) . Среда Visual Studio предложит список шаблонов эле­ ментов. Выберите категорию ASP.NET в панели слева и затем укажите элемент MVC View Page (Страница представления МVС) в центральной панели (рис. 2.11) . ·~ 1nтlle<1 ~J>.NEТ1 Client-side Code f> Onl ine Nomt: MyVi~.csht1n l Sort Ь у: ~•foult i SNrcl1 lnst4JJed Tem~l;tt s ~Ct r!•E} Р· .j MVC (ontroller Clas~ ASP.NEТ ~ Туре: ASP.N H р MVCView Р•9е W!:!b АР\ Contioller Class А5Р.NЕТ MVC Viow Start Page ASP.NEТ ~ ~·. MVC View lmport:; Page ASP.NH ~·~ R.!zor Tag HeJper д5Р.N ЕТ Рис. 2 . 11 . Создание представления Совет. В папке V i e ws уже есть несколько файлов, которые были добавлены Visual Studio с целью предоставления начальн ого содержим ого , показанного на рис. 2.7 . Можете проиг­ норировать эти файлы .
Глава 2. Ваше первое прило жение MVC 39 Введите в поле Name (Имя) имя MyVi ew. cshtml и щелкните на кнопке Add (Доб авить) для создания представления. Среда VisuaJ Studio создаст файл View s/ Home/MyView . c sh tml и откроет его для редактирования. Начальное содержимое файла представления - это просто ряд комментариев и заполнитель. Замените его содержимым, приведенным в листинге 2 .4 . Совет. Довольно легко создать файл представления не в той папке. Если в итоге вы не по­ лучили файл по имени MyView . cshtml в папке Views/Home , тогда удалите созданный файл и попробуйте создать заново . Листинг 2.4. Замена содержимого файла МyView. cshtml из папки Views/Horne @{ Layout = nu ll; < ! DOCTYP E html> <html> <head> <meta name= " v iewport " content= " width=device-width" /> <title>Index</title> </head> <body> <div> Hello World (from the view) </div> </body> </html> Теперь файл представления содержит главным образо м НТМL-разметку. Исклю­ чением является часть, которая имеет следующий вид: @{ Layout null; Такое выражение будет интерпретироваться механизмом визуализации Razor, который обрабатывает содержимое представлений и генерирует НТМL-р азметку, отправляемую браузеру. Показанное выше простое выражение Razor сообщает ме­ ханизму Razor о том, что компоновка не применяется; это похоже на шаблон для НТМL-разметки, который посылается браузеру (и будет описан в главе 5). Мы пока проигнорируем механизм Razor и возвратимся к нему позже. Чтобы увидеть создан­ ное представление, выберите в меню Debug пункт Start Debugging для запуска прило­ жения. Должен получиться результат, приведенный на рис. 2.12 . При первом редактировании метод действия I ndex () возвращал строковое зна­ чение . Это означало, что инфраструктура MVC всего лишь передавала брауз еру стро­ ковое значение в том виде, как есть. Теперь. когда метод Index () возвращает объект ViewResul t, инф раструктура MVC визуализирует представление и возвращает сгене­ рированную НТМL-разметку. Мы сообщили инфраструктуре МVС о том, какое пред­ ставление должно использоваться, поэтому с помощью соглашения об именовании
40 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC она автоматически выполнила его поиск. Соглашение предполагает, что имя файла представления совпадает с именем метода действия, а файл представления хранится в папке, названной по имени контроллера: /Views/Home/MyView . cshtml . Кроме строк и объектов ViewResul t методы действий могут возвращать другие ре­ зультаты . Например, если мы возвращаем объект RedirectResul t, то браузер будет перенаправлен на другой URL. Если мы возвращаем объект HttpUnauthorizedResul t , то вынуждаем пользователя войти в систему. Все вместе такие объекты называются результатами действий. Система результатов действий позволяет инкапсулировать и повторно использовать часто встречающиеся ответы в действиях. В главе 17 мы рассмотрим их подробнее и продемонстрируем разные способы их применения. Hello Wo1·id (fl'OШ tl1e vie\v) Рис. 2.12. Тестирование представления Добавление динамического вывода Весь смысл платформы для разработки веб-приложений состоит в конструирова­ нии и отображении динамического вывода. В рамках МVС работа контролле ра заклю­ чается в подготовке данных и передаче их представлению, которое отвечает за их визуализацию в виде НТМL-разметки. Один из способов передачи данных из контроллера в представление предусмат­ ривает использование объекта ViewBag , который является членом базового класса Controller. По существу ViewBag - это динамический объект, в котором можно устанавливать произвольные свойства, делая их значения доступными в любом визу­ ализируемом далее представлении. В листинге 2.5 демонстрируется передача таким способом простых динамических данных в файле HomeController. cs . Листинг 2.5. Установка данных представления в HomeController. cs using System; using Mic r osoft. AspNetCore .Mv c ; namespace Partyinvites . Contro llers puЫic class HomeCon t roller : Con tro ller puЫic ViewResu l t Index() { int hour = DateTime.Now.Hour; ViewBag. Greeting = hour < 12 ? "Good Morning" return View( "MyView " ) ; "Good Afternoon";
Глава 2. Ваше первое приложение MVC 41 Данные для представления предоставляются во время присваивания значения свойству ViewBag . Greeting. Свойство Greeting не существует вплоть до момента, когда ему присваивается значение - это позволяет передавать данные из контроллера в представление в свободной и гибкой манере, без необходимости в предварительном определении классов. Чтобы получить значение данных, необходимо еще раз сослаться на свойство ViewBag. Greeting, но уже в представлении, как показано в листинге 2.6, содержащем изменение, которое было внесено в файл MyView. cshtml . Листинг 2.6. Извлечение значения данных ViewBag в файле МyView. cshtml @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name =" viewpor t" content="width=device-width" /> <tit l e> I ndex</title> </head> <body> <div> @ViewBag.Greeting World (from the view) </div> </body> </html> Добавленный в листинге фрагмент - это выражение Razor, которое оценивается, когда MVC применяет представление для генерации ответа. Вызов метода View () в методе I ndex ( ) контроллера приводит к тому, что МVС находит файл представления MyView . cshtml и запрашивает у механизма визуализации Razor синтаксический анализ содержимого этого файла. Механизм Razor ищет выражения, подобные до­ бавленному в листинге 2 .6, и обрабатывает их. В рассматриваемом примере обработ­ ка выражения означает вставку в представление значения, которое было присвоено свойству ViewBag . Greeting в методе действия. Выбор для свойства имени Greeting не диктуется никакими особыми соображе­ ниями. Его можно было бы заменить любым другим именем, и все работало бы точно так же при условии, что имя, используемое в контроллере, совпадает с именем, ко­ торое применяется в представлении. Присваивая значения более чем одному свойс­ тву, можно пер едавать из контроллера в представление множество значений данных. После запуска проекта можно увидеть результат внесенных изменений (рис. 2.13). Рис. 2.13. Динамический ответ, сгенерированный MVC
42 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Создание простого приложения для ввода данных В оставшихся разделах этой главы будут исследованы другие базовые функцио­ нальные средства MVC за счет построения простого приложения для ввода данных . В этом разделе мы собираемся несколько увеличить темп изложения. Целью является демонстрация инфраструктуры МVС в действии, поэтому некоторые объяснения того . что происходит "за кулисами", будут пропущены. Однако не беспокойтесь - мы вер­ немся к подробному обсуждению этих тем в последующих главах. Предварительная настройка Представьте себе. что ваша подруга решила организовать вечеринку в канун ново ­ го года и попросила создать веб-приложение, которое позволяет приглашенным отве­ тить на приглашение по электронной почте. Она высказала пожелание относительно четырех основных средств, которые перечислены ниже: • домашняя страница. отображающая информацию о вечеринке; • форма , которая может использоваться для ответа на приглаш е ние (repoпdez s'il vous plalt- RSVP); • проверка достоверности для формы RSVP, которая будет отображать страницу с выражением благодарности за внимание; • итоговая страница. которая показывает, кто собирается прийти на вечеринку . В последующих разделах мы достроим проект МVС, созданный в начале главы , и добавим в него перечисленные выше средства. Первый пункт можно убрать из спис­ ка. применив то, что было показано ранее - добавить НТМL-разметку с подробной информацией о вечеринке в существующее представление. В листинге 2. 7 приведено содержимое файла Views/Home/MyView. cshtml с внесенными дополнениями. Листинг 2.7 . Отображение подробностей о вечеринке в файле МyView. cshtml @{ Layout = null ; <!DOCTYPE html> <html > <head> <meta name="viewport" content= "w i d th=de v ice-w idth" /> <title>Index</title> </head> <body> <div> @ViewBag .Gr eeting World (from the view) <p>We' re going to have an exciting party . <br /> (То do: sell i t better. Add pictures or something.) </р> </div> </body> </html >
Глава 2. Ваше первое приложение MVC 43 Мы двигаемся в верном направлении. Если запустить приложение, выбрав в меню Debug пункт Start Debugging . то отобразятся подробности о вечеринке - точнее, за­ полнитель для подробностей, но идея должна быть понятной (рис. 2.14). 1 Goocl Moш.i11g \Vorld (fi·oщ tl1e Yie,v) \Ve're goiдg to lшУе а11 exc iti.11g party. l(То do' "11 i1 bon« Add piono" 01 •onwd1iug._ ) ______, Ри с. 2.14 . Добавление информации о вечеринке к НТМL-разметке представления Проектирование модели данных Буква "М" в аббревиатуре MVC обозначает тodel (модель) , и она является самой важной частью приложения. Модель - это представление реальных объектов, про­ цессов и правил. которые определяют сферу приложения, известную как предметная область. Модель , которую часто называют моделью предметной области, содержит объекты С# (или объекты предметной области), образующие "вселенную" приложе­ ния, и методы, позволяющие манипулировать ими. Представления и контроллеры открывают доступ клиентам к предметной области в согласованной манере, и любое корректно разработанное приложение МVС начинается с хорошо спроектированной модели, которая затем служит центральным узлом при добавлении контроллеров и представлений . Для про екта Partylnvi tes сложная модель не требуется, поскольку приложение совсем простое, и нужно создать только один класс предметной области, который по­ лучит имя GuestRespo nse. Этот объект будет отвечать за хранение, проверку досто­ верности и подтверждение ответа на приглашение (RSVP). По соглашению MVC классы, которые образуют модель. помещаются в папку по имени Models . Чтобы создать эту папку. щелкните правой кнопкой мыши на проекте Partylnvi tes (элемент. содержащий папки Controllers и Views). выберите в кон­ текстном меню пункт Add~New Folder (ДобавитьqНовая папка) и укажите Models в качестве имени папки. На заметку! Вы не сможете установить имя новой папки , если приложение все еще фун ­ кционирует. Выберите в меню Debug пункт Stop Debugging , щелкните правой кнопкой мыши на элементе NewFolder , который добавился в окно Solution Explorer, выберите в контекстном меню пун кт Rename (Переименовать) и измените имя на Mode ls. Для создания файла масса щелкните правой кнопкой мыши на папке Models в окне Solution Explorer и выберите в контекстном меню пункт AddqClass (ДобавитьqКласс) . Введите GuestResponse . cs для имени нового класса и щелкните на кнопке Add (Добавить). Приведите содержимое нового файла класса к виду , показанному в лис ­ тинге 2.8 .
44 Часть 1. Введение в инфрастру к туру ASP.NET Core MVC Листинг 2.8 . Класс предметной области GuestResponse, определенный в файле Gues tResponse . cs внутри папки Model в namespace Partyinvites . Models { puЫic class GuestResponse { puЫic string Name { get ; set; puЬlic string Email { get; set ; } puЬlic string Phone { get; set; } puЫic bool? WillAttend { get; set; Совет. Вы могли заметить, что свойство WillAttend имеет тип bool, допускающий null, т.е. оно может принимать значение true, false или null . Обоснование этого будет приведено в разделе "Добавление проверки достоверности" далее в главе. Создание второго действия и строго типизированного представления Одной из целей разрабатываемого приложения является включение формы RSVP, а это означает необходимость определения метода действия, который сможет полу­ чать запросы к ней. В единственном классе контроллера может определяться множес­ тво методов действий, а по соглашению связанные действия группируются вместе в одном контроллере. В листинге 2.9 иллюстрируется добавление нового метода дейс­ твия к контроллеру Ноте. Листинг 2.9 . Добавление метода действия в файле HomeController. cshtml using System ; using Microsoft . AspNetCore . Mvc ; namespace Partyinvites . Controllers puЫic class HomeController : Controller puЬlic ViewResult Index() { int hour = DateTime . Now.Hour ; ViewBag. Greeting = hour < 12 ? " Good Morning " return View( "MyView " ); puЫic ViewResul t RsvpForm () return View () ; "G ood Afternoon"; Метод действия RsvpForm () вызывает метод View () без аргументов, что сообща­ ет инфраструктуре МVС о необходимости визуализации стандартного пр едставления, связанного с этим методом действия, которым будет представление с таким же име­ нем, как у метода действия (RsvpForm . cshtml в данном случае).
Глава 2. Ваше первое приложение MVC 45 Щелкните правой кнопкой мыши на папке Views внутри Ho me и выберите в кон­ текстном меню пункт Add<=:> New ltem (Добавить<=:> Новый элемент). Выберите шаб­ лон MVC View Page (Страница представления MVC) из категории ASP.NET, укажи­ те RsvpForm . cshtml в к ачестве имени нового файла и щелкните на кнопке Add (Добавить), чтобы создать файл. Приведите содержимое нового файла в соответствие с листингом 2.10. Листинг 2.10. Содержимое файла RsvpForm. cshtml из папки Views/Home @model Partyinvi tes . Mode ls. Gue stRespon se @{ Layout = null; < !DOCTYPE html > <html> <head> <meta name="viewport" cont ent="width=devi ce- wi dth" /> <title>RsvpForm</titl e> </ head> <body> <div> This is the RsvpForm . c s html View </di v> </body> </html> Содержимое состоит в основном из НТМL-разметки , но с добавлением Rаzоr ­ выражения @model , которое используется для создания строго mипизирован.н.ого представления. Строго типизированное представление предназначено для визуали ­ зации специфического типа модели, и если указан жела емый тип (в данном случае класс GuestResponse из пространства имен Par t yinvite s . Models), то MVC может создать ряд удобных сокращений, чтобы сделать его проще. Вскоре мы задействуем преимущество характеристики строгой типизации. Чтобы протестировать новый метод действия и его представление , запустите при­ ложение, выбрав в меню Debug пункт Start Debugging , и с помощью брауз ера перей ­ дите на URL вида / Home/RsvpForm. Инфраструктура МVС применит описанное ранее соглашение об именовании для направления запроса методу действия RsvpForm () , определенному в контролле­ ре Ho me . Этот метод действия указывает MVC о том, что должно визуализировать­ ся стандартное представление, которое посредством еще одного применения того же соглашения об именовании визуализирует Rs vpFo rm. cs hm l из папки Views/ Home . Результат показан на рис. 2.15. 1· RsvpForm х г---·-----"·~-·-- ~ С Lq:> loca l ho~t 5}628/Home/RsvpFo r m.___ [ " ; ;, ;he Rs-;;pi~''" oshtщl Vi_e,_:s .' ---- --- · Рис. 2.15 . Визуализация второго представления
46 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Ссылка на методы действий Нам необходимо создать в представлении MyView ссылку, чтобы гости могли ви­ деть представление Rsvp Form без обязательного знания URL, который указывает на специфический метод действия (листинг 2.11). Листинг 2.11. Добавление ссылки на форму RSVP в файле МyView. cshtml @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> @ViewBag.Greeting World (from the view) <p>W e're going to have an exciting party.<br /> (То do: sell it better. Add pictures or something.) </р> <а asp-action="RsvpForm">RSVP Now</a> </div> </body> </html> В листинге 2.11 добавлен элемент а. который имеет атрибут asp - action. Данный атрибут является примером атрибута дескрuпторного вспомогательного класса, т.е . инструкцией Razor, которая будет вьшолнена, когда представление визуализируется . Атрибут asp-action - это инструкция по добавлению к элементу а атрибута href, содержащего URL для метода действия. Работа дескрипторных вспомогательных клас­ сов объясняется в главах 24, 25 и 26, а пока достаточно знать, что asp-action - про­ стейший вид атрибута дескрипторного вспомогательного класса для элементов а. Он указывает Razor на необходимость вставки URL для метода действия, определенного в том же контроллере, для которого визуализируется текушее представление. Запустив проект, можно увидеть ссылку, которую создал вспомогательный класс (рис. 2.16). 1 lndex j Rsv pform Х _____ '? i9?~°.~~~~~-;~3___ L Пu $ is 111 ~ RsYpF011n.c slitшl \ ' ie\\· ~--~- j' ~---- Рис. 2.16. Ссылка на метод действия
Глава 2. Ваше первое приложение MVC 47 После запуска приложения наведите курсор мыши на ссылку RSVP Now (Ответить на приглашение) в окне браузера. Вы заметите, что ссылка указывает на следующий URL (возможно , вашему проекту Visual Studio назначит другой номер порта): http://localhost : 57628/Home/R s vpForm Здесь требуется соблюдать один важный принцип: вы должны использовать средс­ тва , предлагае мые МVС для генерации URL, а не жестко кодировать их в своих пред ­ ставлениях. Когда вспомогательный класс создает атрибут href для элемента а , он инсп е ктирует конфигурацию приложения, чтобы выяснить , к аким должен быть URL. В итоге появляется возможность изменять конфигурацию приложения для поддер жки разных форматов URL без необходимости в обновлении каких-либо пр едставлений . Особенности работы этого рассматриваются в главе 15. Построение формы Теперь, когда строго типизированное представление создано и достижимо из представ­ ления I ndex, займемся подгонкой содержимого файла RsvpForm . c s html, чтобы превра ­ тить его в НТМL-форму для редактирования объектов Gu e stResponse (листинг 2.12) . Листинг 2.12. Создание представления в виде формы в файле RsvpForm. cshtml @model Pa r tyinvites . Models . GuestRe sponse @{ Layout = null ; < !DOCTYPE html > <html> <head> <meta name= " viewpo r t " content= " width=devi ce - wi dth " /> <title>RsvpForm</title> </head> <body> <form asp-action="RsvpForm" method="post"> <р> <label asp-for="Name">Your name:</label> <input asp-for="Name" /> </р> <р> <laЬel asp-for="Email">Your email:</laЬel> <input asp-for="Email" /> </р> <р> <laЬel asp-for="Phone">Your phone:</label> <input asp-for="Phone" /> </р> <р> <laЬel>Will you attend?</laЬel> <select asp-for="WillAttend"> <option value=" ">Choose an option</option> <option value="true">Yes, I '11 Ье there</option> <option value="false">No, I can•t come</option> </select> </р> <button type="submit">SuЬmit RSVP</button> </form> </body> </html>
48 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Для каждого свойства IUiacca модели GuestResponse определены элементы label и input (или элемент select в случае свойства Wi llAttend}. Каждый элемент ассо­ циирован со свойством модели с применением еще одного атрибута дескрипторно­ го вспомогательного IUiacca - asp - for. Атрибуты дескрипторных вспомогательных IUiaccoв конфигурируют элементы, чтобы привязать их к объекту модели. Вот пример НТМL-разметки , которую генерируют дескрипторные вспомогательные IUiaccы для от­ правки браузеру: <р> <labe l for="Name">Your name : </label> <input type="text" id="Name" name="Name" value=""> </р> Атрибут asp-for в элементе label устанавливает значение атрибута for. Атрибут asp - for в элементе inp ut устанавливает атрибуты id и name . В данный момент это не выглядит особенно полезным , но по мере определения прикладной функциональ­ ности вы увидите , что ассоциирование элементов со свойством модели предлагает дополнительные преимущества. Более непосредственный результат дает атрибут asp - action в элементе form, который использует конфигурацию маршрутизации URL приложения для установки атрибута action в URL, нацеленный на специфический метод действия, например : <form method= "post " act i on=" /Hom e /RsvpForm " > Как и в случае атрибута дескрипторного вспомогательного класса, примененного к элементу а, преимущество такого подхода заIUiючается в том, что вы можете изме­ нять систему URL, используемую приложением, и содержимое , которое ге нерируется дескрипторными вспомогательными классами, автоматически отразит изменения. Форму можно увидеть, запустив приложение и щел кнув на ссылке RSVP Now (рис. 2.17). @-------"·--·-·-· ·----- · ·------------·-"". ~ С localhost :57628/ Home/RsvpForm ~ ------------------------ Уош 11щnе: ; [ .""."---·-··----· --· "." --- ·--·- ·-···"·- -·-·- -·-_ J Уонr eшail : L 1 ______ ,_"____ .___, Уонr pl1011e: с--------1 \\ ill уон a"end? rch~~e ;~;; ~ ·---· --- ---- ----- --· Рис. 2.17 . Добавление НТМL-формы к приложени ю
Глава 2. Ваше первое приложение MVC 49 П олучение данных формы Мы пока еще не указали инфрастру~<туре MVC , что должно быть сделано, когда форма отправляется серверу. В нынешнем состоянии приложения щелчок на кноп­ ке Submit RSVP (Отправить RSVP) лишь очищает любые значения, введенные в фор­ му . Причина в том , что форма осуществляет обратну~о отправ1<у методу действия RsvpForm () контроллера Home , который толыю сообщает MVC о необходимости пов­ торной визуализации представления. Чтобы получить и обработать отправленные данные формы, мы собираемся вос­ пользоваться основной возможностью контроллера. Мы добавим второй метод дейс­ твия RsvpForm () ,чтобы получить в свое распоряжение следующее. • Метод, который отвечает н.а НТТР-запросы GE T. Каждый раз, 1<огда кто-то щел­ кает на ссьmке, браузер обычно выдает запрос GET. Эта версия действия будет отвечать за отображение изначально пустой формы , когда ~по-нибудь впервые посещает / Ho me/ RsvpFo rm. • Метод, который отвечает на НТГР-запросы PO ST. По умолчанию формы, ви­ зуализированные с помощью Ht ml. Be g in Form () ,отправляются браузером как запросы POST. Эта версия действия будет отвечать за получение отправленных данных и принятие решения о том, что с ними делать. Обработка запросов GET и POST в отдельных методах С# способствует обеспечению аккуратности кода контроллера, т.к. эти два метода имеют разные обязанности. Оба метода действий вызываются через тот же самый URL, но в зависимости от вида за­ проса - GET или POS T - инфраструктура МVС вызьmает подходящий метод. В листин­ ге 2 . 13 показаны изменения, которые необходимо вне сти в класс HomeCo ntro ller. Листинг 2.13. Добавление метода действия для поддержки запросов POST в файле HomeController. cs using Sys tem ; usi ng Mi crosoft .AspNetCore .Mvc; using Partyinvites.Models; name space Partyinvi tes. Controller s puЫic class HomeC ontroller : Contr olle r puЫi c ViewResul t Index( ) { int hour = DateTime . Now .Hour ; ViewBag.Greeting = hour < 12 ? "Good Morning" ret urn View("MyView" ); [HttpGet] puЫic ViewResul t RsvpForm() { return View(); [HttpPost] "Good Afternoon"; puЬlic ViewResult RsvpForm(GuestResponse guestResponse) //Что сделать: сохранить ответ от гостя return View () ;
50 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Существующий метод действия RsvpFor m ( ) был снабжен атрибутом Htt pGet, который указывает MVC на то, что данный метод должен применяться только для запросов GET . Затем была добавлена перегруженная версия метода Rsvp Form ( ) ,при­ нимающая объект Gu e s tRe s ponse. К ней был применен атрибут Http Post , который сообщает МVС о том, что этот новый метод будет иметь дело только с запросами POST. Произведенные добавления объясняются в последующих разделах. Кроме того, было импортировано пространство имен Pa r t y i n v i t es. Mo del s . Это сделано для то г о, чтобы на тип модели Gues t Respon se можно было ссылаться без н е обходимости в указании полностью определ е нного имени класса. Использование привязки модели Первая перегруженная версия метода действия Rs v p Form () визуализирует то же самое представление, что и ранее (файл RsvpForm . cs h t ml), для генер ации формы, показанной на рис. 2.17. Вторая перегруженная версия более интересна из -за нали­ чия параметра. Но с учетом того, что этот метод действия будет вызываться в ответ на НТГР-запрос POST, а тип Gu estRes po nse является классом С# , каким образом они соединяются между собой? Секрет кроется в привязке модели - чрезвычайно полезной функциональной воз­ можности МVС, посредством которой производится разбор входящих данных и при­ менение пар "ключ/значение " в НТГР-запросе для заполнения свойств в типах моде­ лей предметной области. Привязка модели - мощное и настраиваемое средство, которое избавляет от кро­ потливого и тяжелого труда по взаимодействию с НТГР-запросами напрямую и поз­ воляет работать с объектами С#, а не иметь дело с индивидуальными значениями данных, отправляемыми браузером. Объект Guest Resp o ns e , который передается этому м етоду действия в качестве параметра, автоматически заполня ется данными из полей формы. Привязка модели, включая ее настройку, подробно рассматривается в главе 26. Одной из целей приложения является предоставление итоговой страницы с дета­ лями о том, кто придет на вечеринку, что означает необходимость сохранения полу­ чаемых ответов. Мы собираемся делать это с помощью созданной в памяти коллекции объектов . В реальном приложении такой подход не подойдет, т.к. данные ответов бу­ дут утрачиваться в результате останова или перезапуска приложения , но он позволя­ ет сосредоточить внимание на MVC и создать приложение, которое может быть легко сброшено в свое начальное состояние. Совет. В главе 8 будет продемонстрировано использование MVC для постоянного хранения и доступа к данным как часть более реалистичного примера приложения. Мы добавили в проект новый файл, щелкнув правой кнопкой мыши на папке Mo d e l s и выбрав в контекстном меню пункт Add9Class (Добавить9Класс). Файл име­ ет имя Repo si tory . cs и содержимое, показанное в листинге 2.14 . Листинг 2.14 . Содержимое файла Reposi tory. cs из папки Models u s ing System . Collectio n s .Gene ric; namesp ace Part yinvites.Models { puЫ ic static clas s Repository p r ivate static List<Gu es t Res ponse> responses new Li s t<G ue s tResponse>() ;
Глава 2. Ваше первое прило же ние MVC 51 puЫic static IEnumeraЫe<GuestResponse> Responses { get { return responses; puЫic static void AddResponse(GuestResponse resp onse) { responses.Add(response); Класс Repos i tory и его члены объявлены статическими, чтобы облегчить сохра­ нение и извлечение данных из разных мест приложения. Инфраструктура МVС пред­ лагает более сложный подход к определению общей функциональности, называемый внедрением зависимостей, который будет описан в главе 18, но для простого прило­ ж е ния врод е рассматриваемого вполне достаточно и статического RЛасса. Сохранение ответов Теперь , когда есть куда сохранять данные, можно обновить метод действия, кото­ рый получает НТГР-запросы POST (листинг 2.15). Листинг 2.15. Обновление метода действия в файле HomeController. cs using System; using Microsoft . AspNetCore . Mvc ; using Partyinvites .M ode l s; namespace Partyinvites . Controlle rs puЫic class HomeContro l ler : Controller puЫic ViewResult Index() { int hour = DateTime .Now . Hour ; ViewBag . Greeting = h our < 12 ? "G ood Morning" return View ( "MyView " ) ; [HttpGet] puЫic ViewResult RsvpForm() { return View () ; [HttpPost] " Good Afternoon "; puЫic ViewResult RsvpForm(GuestResponse g uest Response) { Repository.AddResponse(guestResponse); return View ( "Thanks", guestResponse) ; Все, что необходимо сделать с данными формы , посланными в запросе - это передать методу Repos i tory. AddResponse () в качестве аргумента объе1п GuestResponse , который был передан методу действия, чтобы ответ мог быть сохранен.
52 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Почему привязка модели не похожа на инфраструктуру Web Forms В главе 1 было указано , что одним из недостатков традиционной инфраструктуры ASP.NET Web Forms было сокрытие деталей НТТР и HTML от разработчиков . Вас может интересовать , делает ли то же самое привязка модели MVC, которая применялась для создания объекта GuestResponse из НТТР-запроса POST в листинге 2.15. Нет, не делает. Привязка модели освобождает нас от решения утомительной и подверже нной ошибкам задачи по инспектированию НТТР-запроса и извлечению все х требующихся значений данных, но (что самое важное) если мы хотим обрабатывать запрос вручную, то могли бы это делать, поскольку MVC предоставляет легкий доступ ко всем данных запроса. Ничто не скрыто от разработчика, но есть несколько удобных средств, которые упрощают работу с НТТР и HTML; тем не менее , использовать их вовсе не обязательно. Это может показаться едва заметной разницей, но по мере углубления знаний инфраструктуры MVC вы увидите, что практика разработки в ней полностью отличается от традиционной инфра­ структуры Web Forms и вы всегда осведомлены о том, как обрабатываются получаемые прило­ жением запросы . Вызов метода View () внутри метода действия RsvpForm () сообщает МVС о том, что нужно визуализировать представление по имени Thanks и передать ему объект GuestResponse. Чтобы создать это представление, щелкните правой кнопкой мыши на папке Views/Home в окне Solution Explorer и выберите в контекстном меню пункт Addc:::>New ltem (Добавить<=:> Новый элемент). Укажите шаблон MVC View Page (Страница представления MVC) из категории ASP.NET, назначьте ему имя Thanks. cshtml и щелкните на кнопке Add (Добавить) . Среда Visual Studio создаст файл Views/Home/ Thanks . cshtml и от1<роет его для редактирования. Поместите в файл содержимое, приведенно е в листинге 2.16. Листинг 2.16. Содержимое файла Thanks. cshtml из папки Views/Home @model Partyinvites.Models.GuestResponse @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name="viewport" content= " width=device - width " /> <title>Thanks</title> </head> <body> <р> <hl>Thank you , @Model . Name!</h l > @if (Model.WillAttend == true) { @:It' s great that you ' re coming . The drinks are already in the fridge! else { @: Sorry to hear that you can ' t make it, but thanks for letting us know. } </р> <p>Click <а asp - action= " ListResponses " >here</a> to see who is coming . </р> </body> </html>
Глава 2. Ваше nервое приложение MVC 53 Представление Thanks. csh t ml применяет механизм визуализации Razor для отображения содержимого на основе значения свойства Gu es t Re s p o ns e , которое пе­ редается методу View () внутри Rsvp Fo r m () . Выражение @mode l синтаксиса Razor указывает тип модели предметной области, с помощью которого представление стро ­ го типизировано. Для доступа к значению свойства в объекте предметной области используется конструкция Model . ИмяСвойст в а . Например, чтобы получить значение свойства Name , применяется Mo d e l. Na me . Не беспокойтесь, если синтаксис Razor пока не по­ нятен - он более подробно объясняется в главе 5 . Теперь. когда создано представление Th a nks, появился работающий базовый при­ мер обработки формы посредством МVС. Запустите приложение в Visual Studio, вы­ брав в меню Debug пункт Start Debugg ing, щелю-rите на ссылке RSVP Now, введите на форме какие-нибудь данные и щелкните на кнопке Submit RS VP. Вы увидите резуль­ тат, показанный на рис. 2.18 (он может отличаться, если введено другое имя либо указано о невозможности посетить вечеринку). Thank you, Joe! I Yat1r pl1011e: ~5.~:~234===.:._ -=] It's gi·e~ r tlint yat1'r~ co111ing. llie drщks are already iн tl1e fr1dge! 1 1 \\~i l l уа11 ntt e11d? г~;~ ii~i~e~~ ~~-- Click~ !О see \V!IO is сошiнg. ~uьmit R~~-~ L______Г_____________________ _J [___ ---- ------------------ -- --- --- -- --- -- ------ _, Рис. 2.18. Представление Th a n ks Отображение ответов В конце представления Th a nks. c sht ml мы добавили элемент а для создания ссыл­ ки. которая позволяет отобразить список людей, собирающихся посетить вечеринку . С применением атрибута дескрипторного вспомогательного класса a s p -actio n со­ здается URL, который нацелен на метод действия по имени Lis t Re s pon ses (): <p>C l ick <а asp-action="ListResponses" >h ere</a > to see who is coming.< /p> Наведя курсор мыши на с сылку, rшторую отображает браузер, вы заметите, что она указывает на URL вида / Ho me/Lis tRe s pon s e s . Это не соответствует ни одно­ му методу действия в контроллере Н оте , и если вы щелкнете на ссылке, то увиди ­ те пустую страницу. Открыв инструменты разработки браузера и просмотрев от­ вет, присланный сервером, легко обнаружить , что сервер отправил обратно ошибку 404 - Not Foun d (404 - не найдено) . (Браузер Chrome несколько странен тем, что не отображает сообщение об ошибке для пользователя, но в главе 14 будет показано, как генерировать содержательные сообщения об ошибках.}
54 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Мы устраним проблему путем создания в контроллере Home метода действия, на который нацелен URL (листинг 2 .1 7). Листинг 2.17. Добавление метода действия в файле HomeController. cs using System; using Microsoft.AspNetCore . Mvc ; using Partyinvites.Models; using System.Linq; namespace Partyinvites . Controllers puЫic class HomeCo ntroller : Controller puЫic ViewResult Index() { int hour = DateTime .N ow .H our; ViewBag. Greeting = hour < 12 ? "Go od Morning " return View ( "MyView " ) ; [HttpGet] puЫic ViewResult RsvpForm() { return View(); [HttpPost] "Good Afternoon "; puЫic ViewResult RsvpForm(GuestResponse guestResponse) { Repository . AddResponse(guestResponse); return View ( "Thanks", guestResponse) ; puЫic ViewResult ListResponses() { return View(Repository.Responses.Where(r => r.WillAttend == true)); Новый метод действия называется ListResponses ();он вызывает метод View (), используя свойство Reposi tory. Responses в качестве аргумента. Именно так метод действия предоставляет данные строго типизированному представлению . Коллекция объектов GuestResponse фильтруется с применением LINQ, так что используются только ответы с положительным решением об участии в вечеринке. Метод действия ListResponses не указывает имя представления, которое долж­ но применяться для отображения коллекции объектов GuestResponse, поэтому бу­ дет задействовано соглашение об именовании и MVC инициирует поиск представ­ ления по имени ListResponses. cshtml в папках Views/Home и Views/Shared . Чтобы создать представление, щелкните правой кнопкой мыши на папке Views/ Home в окне Solution Explorer и выберите в контекстном меню пункт AddqNew ltem (ДобавитьQ Новый элемент). Выберите шаблон MVC View Page (Страница представле­ ния MVC) из категории ASP.NET, назначьте ему имя ListResponses. cshtml и щелк­ ните на кнопке Add (Добавить). Приведите содержимое нового файла представления в соответствие с листингом 2.18.
Глава 2. Ваше первое прило жение MVC 55 Листинг 2.18. Отображение принятых приглашений в файле ListResponses. cshtrnl из папки Views/Home @model IEnumeraЬle<Partyinvites.Models . GuestResponse> @{ Layout = null; < !DOCTYPE html > <html> <head> <meta name="viewport " content= " width=device-width " /> <title>Responses</title> </head> <body> <h2>Here is the list of people attending the party</h2> <tаЫе> <thead> <tr> <th>Name</th> <th>Email</th> <th>Phone</ th> </tr> </thead> <tbody> @foreach (Partyinvites.Models.GuestResponse r in Model) { <tr> <td>@r .Name </td> <td>@r .Email</td> <td>@r . Phone</td> </tr> </tbody> </tаЫе> </body> </html> Файлы представлений Razor имеют расширение cshtml, потому что содержат смесь кода С# и элементов HTML. Это можно заметить в листинге 2.18, где исполь­ зуется цикл foreach для обработки всех объектов GuestResponse, которые метод действия передает представлению с применением метода View () . В отличие от нор­ мального цикла foreach языка С# тело цикла foreach из Razor содержит элементы HTML, добавляемые к ответу, который будет отправлен обратно браузеру. В данном представлении для каждого объекта GuestResponse генерируется элемент tr , кото­ рый содержит элементы td, заполненные значениями свойств объекта. Чтобы увидеть список в работе, запустите приложение, выбрав в меню Debug пункт Start Debugging , отправьте какие-то данные формы и затем щелкните на ссыл­ ке для просмотра списка ответов. Отобразится сводка по данным, введенным вами с момента запуска приложения (рис . 2.19). Представление не оформляет данные при­ влекательным образом, но пока этого вполне достаточно, а стилизацией мы займемся позже в главе.
56 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 1· ReJ:ponses х Не1·е is the list of people attending the party Eшail Phon t> Joe joe,rg. exiuнp le.co 111 555- 1234 Al ice al i ce@e.xaшple . coш 555 -56 78 1 -----·--------·--------------·---__J Рис. 2. 19. Отображение списка участников вечеринки Добавление проверки досто верности Теперь мы готовы добавить в приложение проверку достоверности вводимых дан­ ных . В отсутствие проверки достоверности пользователи смогут вводить бессмыслен­ ные данные или даже отправлять пустую форму . В приложении МVС пров ерка до­ стоверности обычно прим еня ется к модели предметной области, а не производится в пользовательском интерфейс е. Это значит, что проверка достоверности определя­ ется в одном месте , но оказывает воздействие в приложении везде, где использует­ ся класс модели. Инфраструктура MVC поддерживает декларативные правила про­ верки достоверности, опр еделенные с помощью атрибутов из пространства имен System . Co mponen t Mode l. DataAnnotations , т.е. ограничения провер ки до стовер­ ности выражаются посредством стандартных атрибутов С#. В листинге 2.19 показа­ но, как применить эти атрибуты к классу модели Gu estRespon se. Листинг 2.19. Применение проверки достоверности в файле GuestResponse. cs using System.Componentмodel.DataAnnotations; names pace Partyinvit es . Models puЫic class GuestResponse { [Required(ErrorMessage = "Please enter your name")] 11 Пожалуйста, введите свое имя puЫic string Name { get ; set ; } [Required(ErrorMessage = "Please enter your email address")] [RegularExpression(".+\\@.+\\ . . +", ErrorMessage = "Please enter а valid email address")] 11 Пожалуйста , введите свой адрес электрон н ой почты puЫic st ring Email { get; set; } [Required (ErrorMessage = "Please enter your phone numЬer")] 11 Пожалуйста, введите свой номер телефона puЫic string Phone { get; set; J [Required (ErrorMessage = "Please specify whether you' 11 attend")] 11 Пожалуйста , укажите , примете ли участие puЫic bool? WillAttend { get; set; }
Глава 2. Ваше первое приложение MVC 57 Инфраструктура МVС автоматически обнаруживает атрибуты проверки достовер­ ности и использует их для проверки данных во время процесса привязки модели. Мы импортировали пространство имен, которое содержит атрибуты проверки достовер­ ности, так что к ним можно обращаться, не указывая полные имена. Совет. Как отмечалось ранее, для свойства WillAttend был выбран булевский тип , допус­ кающий null. Это было сделано для того, чтобы можно было применить атрибут провер­ ки Required. Если бы использовался обычный булевский тип, то значением, получаемым посредством привязки модели, могло быть только true или false, и отсутствовала бы возможность определить, выбрал ли пользователь значение. Булевский тип, допускающий null , имеет три разрешенных значения: true, false и null. Браузер отправляет зна­ чение null, если пользователь не выбрал значение, и тогда атрибут Required сообщит об ошибке проверки достоверности . Это хороший пример того, насколько элегантно ин­ фраструктура MVC сочетает средства С# с HTML и НТТР. Проверку на наличие проблемы с достоверностью данных можно выполнить с применением свойства ModelState. IsValid в классе контроллера. В листинге 2.20 показано, как это реализовано в методе действия RsvpForm (), поддерживающем за­ просы POST, внутри класса контроллера Home. Листинг 2.20. Проверка на наличие ошибок проверки достоверности для данных формы в файле HorneController. cs using System; using Microsoft.AspNetCore.Mvc; using Partyinvites.Models; using System.Linq; namespace Partyinvi tes.Controllers puЫic class HomeController : Controller puЫic ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag. Greeting = hour < 12 ? " Good Morning" return View ( "MyView " ) ; "Good Afternoon"; [HttpGet] puЬlic ViewResul t RsvpForm () { return View () ; [HttpPost] puЫic ViewResult RsvpForm(GuestResponse guestResponse) if (ModelState.IsValid) { Repository.AddResponse(guestResponse) ; return View("Thanks", guestResponse) ; else { 11 Обнаружена ошибRа проверки достоверности. return View(); puЬlic ViewResult ListResponses() { return View(Repository .Response s .Where (r => r.WillAttend true));
58 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Базовый класс Controller предоставляет свойство по имени ModelState, ко­ торое сообщает информацию о преобразовании данных НТГР-запроса в объекты С#. Если свойство ModelState. IsValue возвращает true, то известно, что инфраструк­ тура МVС сумела удовлетворить ограничения пров ерки достоверности, указанные че­ рез атрибуты для класса GuestResponse. Когда такое происходит, визуализируется представление Thanks, как делалось до того. Если свойство ModelState . IsValue возвращает false, то известно, что есть ошибки проверки достоверности. Возвращаемый свойством ModelState объект пре­ доставляет подробные сведения о каждой возникшей проблеме, но нам нет нужды погружаться на такой уровень деталей, поскольку мы можем полагаться на удобное средство, которое автоматизирует процесс указания пользователю на необходимость решения любых проблем, вызывая метод View () без параметров. Когда MVC визуализирует представление, механизм Razor имеет доступ к дета­ лям любых ошибок проверки достоверности, связанных с запросом, и дескриптор­ ные вспомогательные классы могут обращаться к этим деталям, чтобы отображать сообщения об ошибках пользователю. В листинге 2 .21 приведено содержимое файла представления RsvpForm . cshtml с добавленными атрибутами дескрипторных вспо­ м ог ательных классов для проверки дост оверност и. Листинг 2.21. Добавление сводки по проверке достоверности в файл RsvpForm.cshtml @model Partyinvites . Models . GuestResponse @{ Layout = null ; ) < !DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>RsvpForm</ti t le> </head> <body> <form asp-action= " RsvpForm " method="post"> <div asp-validation-summary= 11 All 11 ></div> <р> <label asp-for= " Name " >Your name:</label> <input asp-for="Name " /> </р> <р> <label asp-for= " Email " >Your email : </label> <input asp-for="Email" /> </р> <р> <label asp-for="Phone " >Your phone:</label> <input asp - for= " Phone " /> </р> <р> <label>Will you attend?</label> <select asp-for= " WillAttend " > <option value= "" >Choose an option</option> <option value= " true">Yes , I 'll Ье there</option> <option value= "false">No, I can ' t come</option> </select>
Глава 2. Ваше первое прило же ние MVC 59 </р> <b utton type= " submit " >Submit RSVP</butt on> </form> </body> </html> Атрибут asp - validation - summary применяется к элементу div и отобра­ жает список ошибок проверки достоверности при визуализации представления. Значение для атрибута asp - validat ion-summar y берется из перечисления по име­ ни Val idationSummary, которое указывает типы ошибок проверки достоверности, помещаемые в сводку. Мы указали All, что является хорошей отправной точкой для большинства приложений, а в главе 27 будут описаны другие значения. Чтобы взглянуть, как работает сводка по проверке достоверности, запустите при ­ ложение, заполните поле Name и отправьте форму. не вводя другие данные. Вы уви­ дите сводку с ошибками, показанную на рис. 2.20. R\Vl)Form х 1·~ 1·-·-- · --·---------:i С L ф lo.alhost:S7626/Horпe/Rs11pform '(( 1 -_.----·-~ __" _ --=~·~-:..=--·---=-·::=--::-:--:--- 1 • Рlе ме nller yo11.f ешаi\ acklr~s · • Please enter )'O\lf phone U11J11Ьer 1 • Рlеме specit)· ,, ·he1 /1 o:r yo11'll :11ta1d j Yot1r шuue ; \~~-=--=- __-- · -= - -=] 1 . r- 1 Yo11.f ешмl:l --. . . Уощ pl1011e : с_-=~ · 1 1 \\'1 11 уон att end" : С~оо~е ал opli~? • J 1(Jiiixn;1-RsvEJ [_______:·==--·--·-·----------·-·--·· -----· --·-----J Рис. 2.20. Отображение ошибок проверки достоверности Метод действия RsvpFo r m () не визуализирует представление Than ks до тех пор , пока не будут удовлетворены все ограничения провер1'и достоверности, примененные к классу GuestResponse. Обратите внимание, что введенные в поле Name данные были сохранены и отображены снова при визуализации механизмом Razor представ­ ления со сводкой проверки достоверности. Это еще одно преимущество привязки мо­ дели, и оно упрощает работу с данными формы. На заметку! Если вы работали с ASP.NET Web Forms, то знаете, что в Web Forms имеется концепция серверных элементов управления, которые сохраняют состояние, сериализи­ руя значения в скрытое поле формы по имени VIEWSTATE. Привязка модели MVC не имеет никакого отношения к концепциям серверных элементов управления, обратным от­ правкам или состоянию представления, принятым в Web Forms. Инфраструктура MVC не внедряет скрытое поле VIEWSTATE в визуализированные НТМL-страницы. Взамен она включает данные, устанавливая атрибуты value элементов управления input.
60 Часть 1. Введение в инфраструк туру ASP.NET Core MVC подсветка полей с недопустимыми значениями Атрибуты дескрипторных вспомогательных классов, которые ассоциируют свойс­ тва модели с элементами, обладают удобным средством , которое можно использовать в сочетании с привязкой модели. Когда свойство класса модели не проходит проверку достоверности, атрибуты дескрипторных вспомогательных .классов будут генериро ­ вать несколько отличающуюся НТМL-разметку. Вот элемент input , который генери­ руется для поля Phone при отсутствии ошибок проверки достоверности: <input type= " text " data-val= " true " data - val-req u ired= "Pl ease enter your phone nurnber " id= " Phone " narne=" Phone " value= "" > Для сравнения ниже показан тот же НТМL-элемент после того, .ка.к пользователь отправил форму, не введя данные в текстовое поле (что является ошибкой проверки достоверности, поскольку мы применили к свойству Pho ne класса GuestResponse атрибут проверки Required): <input type= " text " class="input-validation-error" data-val= " true" data-val - required= "Pl ease ente r your phone nurnber" id="Phone " narne= " Phone " va lue="" > Отличие выделено полужирным: атрибут дес.крипторного вспомогательного класса asp-f or добавил .к элементу input .класс по имени input-validation-error . Мы можем воспользоваться этой возможностью, создав таблицу стилей, которая содер­ жит стили CSS для этого класса и другие стили, применяемые различными атрибу­ тами дескрипторных вспомогательных классов. По принятому в проектах МVС соглашению статическое содержимое, доставляе­ мое клиентам, помещается в папку wwwroot , подпапки которой организованы по типу содержимого, так что таблицы стилей CSS находятся в папке wwwroot/css , файлы JavaScrlpt - в папке wwwroot/j s и т.д. Для создания таблицы стилей щелкните правой .кнопкой мыши на папке wwwroot/ css в окне Solution Exp lore r и выберите в контекстном меню пункт AddФNew ltem (ДобавитьФ Новый элемент). В открывшемся диалоговом окне Add New ltem (Добавление нового элемента) перейдите в раздел Client-side (Клиентская сторона) и выберите шаб­ лон Style Sheet (Таблица стилей) из списка (рис. 2.21). r~. !:;~: .litnt·sidt 1 Code !t> Onli~ 1 1 1 f N.:iu1~ L Sort Ьy. Odault 1 CJ HTML P•ge @j5 TyptS<ript Fi!e. l§J' T~Scrip tJ SX F i lc rJ Type5(t1pt JSON Conf19ur111on f1le rJ Sowc1 Confi9u11tion f 1lc C/1cnt ·side Clttnt ·sidt Clicnt·side Cl1cnt·~ide Clltnt-1idc P·j .а. Туре; C!itnHidt 1 А(.tS(adir19 ~ty!t sh<!d. (CS.S) u~cd for1i<h 1 HTML\tyledtfinot1 oros J Q d"d-)o;,-;; -i j --- - __J Рис . 2.21 . Создание таб лицы стилей CSS
Глава 2. Ваше первое приложение MVC 61 Совет. Среда Visual Studio создает файл style . css в папке wwwroot/css при созда­ нии проекта с использованием шаблона Web Application. В этой главе данный файл не задействован. Назначьте файлу имя styl es . css, щелкните на кнопке Add (Добавить), чтобы со­ здать файл таблицы стилей, и приведите его содержимое к виду, показанному в лис­ тинге 2 .22. Листинг 2.22 . Содержимое файла styles. css . field - validation-error {color : #fOO ; } . field - validation- val id { display : none; . input- validation- error { border : lpx solid #fOO ; background-color : #fee ; } . validation- summary - errors { font-weight : bold ; color : #fOO ; } . validation-summary-valid { display : none; } Чтобы применить эту таблицу стилей, мы добавили элемент link в раздел head представления RsvpForm (ли стинг 2.23). Листинг 2.23. Применение таблицы стилей в файле RsvpForrn.cshtml <head> <meta name="viewport " content= " width=device-width" /> <title>RsvpForm</title> <link rel="stylesheet" href="/css/styles.css" /> </head> Элемент link использует атрибут href для у1<азания местоположения таблицы стилей. Обратите внимание, что папка wwwroot в URL опущена. Стандартная кон­ фигурация ASP.NET вrшючает поддержr<у обслуживания статического содержимого, такого как изображения, таблицы стилей CSS и файлы JavaScrlpt , которая автомати­ чески отображает запросы на папку wwwroot. Процесс конфигурации ASP.NEТ и МVС рассматривается в главе 14. Совет. Для работы с таблицами стилей предусмотрен специальный дескрипторный вспомо­ гательный класс, который может оказаться полезным при наличии многочисленных фай­ лов, требующих управления . Подробности ищите в главе 25 . Благодаря применению таблицы стилей в случае отправ1ш данных, которые вызы­ вают ошибки проверки достоверности, отображается визуально боле е ясные сообще­ ния об ошибках (рис. 2.22) . Стилизация содержимого Все цели приложения , касающиеся функциональности, достигнуты , но его общий вид оставляет желать лучшего. Когда вы создаете проект с использованием шаблона Web Application, как в текущем примере , Visual Studio устанавливает несколько рас­ пространенных пакетов для разработки на стороне клиента. Хотя я не являюсь сто­ ронником применения шаблонов проектов, мне нравятся выбранные Microsoft библи-
62 Часть 1. Введение в инфраструктуру ASP.NET Core MVC отеки клиентской стороны. Одна из них называется Bootstrap и представляет собой удобную инфраструктуру CSS, первоначально разработанную в 'I\vitter. которая пос­ тепенно превратилась в крупный проект с открытым кодом и стала главной опорой разработки веб-приложений. Rsvpioim Х ,-- ---- -· -- - ·------·- -----·- . - - - С LФ locJ!lюst:S7628/НQ111e/RsvpForm . ~- ... --~ - . ___ ._. ._." -; -::- ~- _. . , ·-~~:-·- --···- - - : : ·- :·;::::-.:- - ... ·::--..::.:: 1 • Plta8• 1.>ntft' ~·out· t>m.;iiJ addi'\'и • Р1Е-а~.- e11ftr :;1·ощ· phoot' numbtr • Pl•:a~• \pc.>('if·y " 'htther ~·ou'U atttod --- -" ! Yo\U' eшail : ....1-- --- =:J 1 Your pl1oue : 1 1 \ViU >"• '"'""*"""' "''"''" 1 1 1 [ suьn~li'RsiiP ] 1 L____, ___________.__________J Рис. 2.22. Автоматическая подсветка полей с ошибками проверки достоверности На заметку! На момент написания этой книги текущей версией была Bootstrap 3, но версия 4 находилась на стадии разработки. В Microsoft могут принять решение обновить версию Bootstrap, используемую шаблоном Web Application, в последующих выпусках Visual Studio, что может привести к отображению содержимого по-другому. В других главах кни­ ги это не должно стать проблемой, т.к. там будет показано, каким образом явно указать версию пакета , чтобы получить ожидаемые результаты . Стилизация начального представления Базовые средства Bootstrap работают за счет применения классов к элементам, которые соответствуют селекторам CSS, определенным внутри добавленных в папку wwwroot/liЬ/bootstrap файлов. Подробную информацию о классах, определенных в библиотеке Bootstrap, можно получить на веб-сайте http: / / getboo ts trap . с от , а в листинге 2.24 демонстрируется использование нескольких базовых стилей в пред­ ставлении MyView. cshtml. Листинг 2.24 . Добавление классов Bootstrap в файл МyView. cshtml @{ Layout = null; < ! DOCTYPE html>
<html> <head> Глава 2. Ваше первое прило жение MVC 63 <meta name= " vi ewpor t " content=" width=devi ce- width" /> <title>Index</ title> <link rel="stylesheet" href="/lib/Ьootstrap/dist/css/Ьootstrap.css" /> </head> <body> <div class="text-center"> <hЗ>We're going to have an exciting party!</hЗ> <h4>And you are invited</h4> <а class="Ьtn Ьtn-primary" asp-action="RsvpForm">RSVP Now</a> </di v> </body> </html> В разметку был добавлен элемент link, атрибут h r e f которого загружает файл bootstrap . css и з папки wwwroot/liЬ/boot s trap/dist/css . По соглашению пак е ­ ты CSS и JavaScript от независимых поставщиков устанавливаются в папку www roo t / lib, и в главе 6 будет описан инструмент, предназначенный для управления такими пакетами. После импортирования таблиц стилей Bootstrap осталось стилизовать элементы. Рассматрива емый пример прост, поэтому необходимо использовать совсем немного классов CSS из Bootstrap: text - cen ter, Ьtn и Ьtn -p rima r y . Кл асс text - ce n ter центрирует содержимое элемента и его дочерних элементов. Класс Ьtn стилизует эл емент but t on , input или а в виде симпатичной кнопки. а класс Ьtn -primary указывает диапазон цветов для этой кнопки . Запустив приложе­ ние, можно увидеть результат, показанный на рис. 2.23 . Вам должно быть вполне очевидно , что я - не веб-дизайнер . На самом деле, буду­ чи еще ребенком , я был освобожден от уроков рисования по причине полного отсутс­ твия таланта. Это произвело благоприятный эффект в виде того, что я стал уделять больше времени урокам математики, но вместе с тем мои художественные навыки не развивались примерно с десятилетнего возраста. Для реальных проектов я обратился бы к помощи профессионального дизайнера содержимого, но в настоящем примере я собираюсь делать все самостоятельно и применять Bootstrap с максимально возмож­ ной сде ржанностью и согласованностью, на какую толь ко способен. lndex х С !. ф localhost:57628 We're going to have an exciting party! And you are inv ited 11И1Р!~' ______ _______ __ J Рис. 2.23. Стилизация представления
64 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Стилизация представления RsvpForm В Bootstra p опред ел е ны классы , которые могут использоваться для стил из ации фор м . Я н е планирую вдаваться в особые детали, но в листинге 2.25 показано, как были применены эти классы. Листинг 2.25. Добавление классов Bootstrap в файл RsvpForm. cshtml @mode l Partyinvite s. Mode l s . Gu e stRes ponse @{ Layout = null; < !DOCTYPE html > <html> <head> <meta name= " viewport " content= " width= devic e- width " /> <ti tle>RsvpForm</ti t l e> <li nk rel =" stylesheet" href=" /css/styles.css " /> <link rel="stylesheet" href="/lib/Ьootstrap/dist/css/Ьootstrap.css" /> </ h ead> <bod y> <div class="panel panel-success"> <div class="panel-heading text-center"><h4>RSVP</h4></div> <div class="pan e l-Ьody"> <form class="p-a-1" asp-action="RsvpForm" method="post"> <div asp-va lidation-swnmary="All"></div> <div class="form-group"> <laЬe l asp-for="Name">Your name:</laЬ e l> <input class= "form-control" asp-for= "Name" /> </div> <div cla ss= "form-group"> <laЬel asp-for=" Email">Your email:</laЬel> <input class="form-control" asp-for="Email" /> </div> <div class="form-group"> <laЬel asp-for="Phone">Your phone:</laЬel> <input class="form-control" asp-for="Phone" /> </div> <div class="form-group"> <laЬel>Will you attend?</laЬel> <select class="form-control" asp-for="WillAttend"> <option value="">Choose an option</option> <option value="true">Yes, I'll Ье there</option> <option value="false">No, I can ' t come</option> </select> </div> <div class="text-center"> <Ьutton class="Ьtn Ьtn-primary" type="suЬmit"> SuЬmit RSVP </Ьutton> </div> </form> </div> </div> </body> </html>
Глава 2. Ваше первое прило же ние MVC 65 Классы Bootstrap в этом примере создают заголовок, просто чтобы придать компо­ новке структурированность. Для стилизации формы используется класс form-group, J{Оторый стилизует элемент, содержащий label и связанный элемент input или select. Результаты стилизации можно видеть на рис. 2.24 . 1 '~1.. YourpJюne: 1 1 W1ll you attend? Choose an opuon ·- !1 j 1 j 1 1 ! 1 1 L. -----~-·-~------·-----·-----··-·-- ·----- ___j Рис. 2.24. Стилизация представления RsvpForm Стилизация представления Тhanks Следующим стилизуемым представлением является T hanks. cshtml; в листин­ ге 2.26 показано, как это делается с применением классов CSS , подобных тем, которые использовались для других представлений. Чтобы облегчить управление приложени­ ем, имеет смысл избегать дублирования кода и разметки везде, где только возможно . Инфраструктура MVC предлагает несколько средств, помогающих сократить дубли­ рование, которые рассматриваются в последующих главах. К таким средствам отно­ сятся компоновки Razor (глава 5), частичные пр едставления (глава 21) и компоненты представлений (глава 22). Листинг 2.26. Добавление классов Bootstrap в файл Thanks. cshtml @model Partyinvites . Models . GuestResponse @{ Layout = null; <!DOCTYP E html> <h tml> <head > <meta n ame= "viewp ort " content= " width=device - width " /> <title>Thanks</title> <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" /> </head>
66 Часть 1. Введение в и нфрас тр у ктуру ASP.NET Core MVC <body class="text-center"> <р> <hl>Thank you , @Model . Name!</hl> @if (Mode l. WillAttend == true) { @: It ' s great that you ' re coming. Th e drinks are already in the fridge! else { @: Sorry to hear that you can ' t make it, but thanks for letting us know. } </р> Click <а class="nav-link" asp-action="ListResponses">here</a> to see who is coming . </body> </html> На рис. 2.25 показан результат стилизации. Thank you, Joe! ll's great tha t you're coming. The drink s are already in tl1e fridge ' Click t1 ere to see who is coming. -- ------------------------- --J Рис. 2.25. Стилизация представления Th anks Стилизация представления Lis tResponses Последним мы стилизуем представление ListResponses, которое отображает список участников вечеринки . Стилизация содержимого следует тому же ба зовому подходу, который применялся в отношении всех стилей Bootstrap (листинг 2.27). Листинг 2.27. Добавление классов Bootstrap в файл ListResponses. cshtml @model IEnumeraЬle<Partyinvites.Models . Gue s tResponse> @{ Layout = null ; <!DOCTYPE html> <html> <h ead> <meta name= " viewport " content= " widt h=device-widt h" /> <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" /> <tit l e>Responses</t itle> </head> <body> <div class="panel-body"> <h2>Here is the l ist of people attending the party</h2>
Глава 2. Ваше nервое nриложение MVC 67 <tаЫе class="taЫe taЫe-sm taЫe-striped taЫe-Ьordered"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Phone</th> </tr> </ the ad> <tbody> @foreach (Partyinvites .Mo dels . GuestResponse r in Mode l) { <tr> <td>@r.Name</td> <td>@r . Email</td> <td>@r . Phone</td> </tr> < /tbody> </tаЫе> </div> </body> </html> На рис . 2.26 показано, как теперь выглядит таблица участников . Добавление сти­ лей к данному представлению завершает пример приложения , которое теперь достиг­ ло всех целей разработки и имеет намного более совершенный внешний вид. if- 1[~ Here is the list of people attending the party ! 1 Name 1 Joe 1 Alico i ВоЬ ! Emai l joe@example.com a1ice ~ example .com bob @example.com [______ - ---·-- --- Phone 555-1 234 255-2345 i1 1 1 i - -·- ___ J Рис. 2.26. Стилизация представления ListResponses Резюме В этой главе был создан новый проект МVС , который использовался для построе ­ ния простого приложения ввода данных МVС, что позволило получить первое пред­ ставление об архитектуре ASP.NET Core MVC и применяемом подходе. Некоторые о сн о вные средства (включая синтаксис Razor, маршрутизацию и тестирование) не рассматривались, но мы вернемся к этим темам в последующих главах. В следующей глав е будет описаны паттерны проектирования MVC, которые формируют основу эф­ фективной разр аботки с помощью ASP.NET Core МVС .
ГЛАВА 3 Паттерн МVС, проекты и соглашения п режде чем приступить к углубленному изучению деталей инфраструктуры ASP.NEТ Core MVC, необходимо освоить паттерн проектирования МVС, лежащие в его основе концепции и способ, которым они транслируются в проекты ASP.NET Core MVC. Возможно, вы уже знакомы с некоторыми идеями и соглашениями, обсуждае­ мыми в этой главе, особенно если вам приходилось заниматься разработкой сложных приложений ASP.NET или С#. Если это не так, тогда внимательно читайте настоящую главу. Хорошее понимание того, что положено в основу MVC, может помочь увязать функциональные возможности инфраструктуры с контекстом материала, излагаемо­ го в оставшихся главах книги. История создания MVC Термин модель-представление-контроллер (model-view-controller) был в употреб­ лении с конца 1970-х годов и происходит из проекта Smalltalk в Xerox PARC, где он был задуман как способ организации ряда ранних приложений с графическим поль­ зовательским интерфейсом. Некоторые нюансы первоначального паттерна MVC были связаны с концепциями, специфичными для Smalltalk, такими как экраны и инстру­ менты, но более широкие понятия по-прежнему применимы к приложениям - и осо­ бенно хорошо они подходят для веб-приложений. Особенности паттерна MVC Если оперировать высокоуровневыми понятиями, то паттерн MVC означает, что приложение MVC будет разделено, по крайней мере, на три указанные далее части. • Модели, содержащие или представляющие данные, с которыми работают пользователи. • Представления, используемые для визуализации некоторой части модели в виде пользовательского интерфейса. • Контроллеры, которые обрабатывают входящие запросы, выполняют операции с моделью и выбирают представления для визуализации пользователю. Каждая порция архитектуры MVC четко определена и самодостаточна; такое по­ ложение вещей называют разделением обязанностей. Логика, которая манипулиру-
Глава 3. Паперн MVC, проекты и соглашения 69 ет данными в модели, содержится только в модели. Логика, отображающая данные, присутствует только в представлении. Код, который обрабатывает пользовательские запросы и ввод. находится только в контроллере. Благодаря ясному разделению между порциями приложение будет легче сопровождать и расширять на протяже­ нии его времени существования вне зависимости от того, насколько большим оно станет. Понятие моделей Модели (Мв МVС) содержат данные, с которыми работают пользователи. Сущест­ вуют два обширных типа моделей: модели представлений, которые выражают сами данные, передаваемые из контроллера в представление. и модели предметной облас­ ти, которые содержат данные в предметной области наряду с операциями. транс­ формациями и правилами для создания, хранения и манипулирования данными, все вместе называемыми логикой моделей. Модели - это определение "вселенной", в которой функционирует приложение. Например. в банковском приложении модель представляет все аспекты банковской деятельности, поддерживаемые приложением, такие как расчетные счета, главная бухгалтерская книга и кредитные лимиты для клиентов. равно как и операции, ко­ торые могут применяться для манипулирования данными в модели. подобные вне­ сению денежных средств и списанию их со счетов . Модель отвечает также за сохра­ нение общего состояния и целостности данных - например, удостоверяясь, что все транзакции внесены в главную книгу, а клиент не снимает со счета больше денеж­ ных средств, чем имеет на то право, или больше. чем находится в распоряжении самого банка. Для каждого компонента в паттерне MVC будет описано, что он должен включать, а что не должен. Модель в приложении, построенном с использованием паттерна МVС, доЛJ1Сна: • содержать данные предметной области; • содержать логику для создания, управления и модификации данных предмет­ ной области; • предоставлять чистый АРI-интерфейс, который открывает доступ к данным мо­ дели и операциям с ними. Модель не должна: • показывать детали того, ка~< осуществляется получение или управление дан­ ными модели (другими словами, подробности механизма хранения данных не должны быть видны контроллерам и представлениям); • содержать логику. которая трансформирует модель на основе взаимодействия с пользователем (поскольку это работа контроллера): • содержать логику для отображения данных пользователю (т.к. это работа представления). Преимущества обеспечения изоляции модели от контроллера и представлений за­ ключаются в том, что вы можете гораздо легче тестировать логику (модульное тести­ рование описано в главе 7) и проще расширять и сопровождать приложение в целом.
70 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Совет. Многих разработчиков, только приступивших к ознакомлению с паттерном MVC, при­ водит в замешательство идея помещения логики в модель данных из - за их уверенности в том, что целью паттерна MVC является отделение данны х от логики . Это заблуждение: цель паттерна MVC - разделение приложения на три функциональных области, каждая из которых может содержать и логику, и данные. Цель не в том, чтобы устранить логику из модели. Наоборот, цель в том, чтобы гарантировать наличие в модели только логики, предназначенной для создания и управления данными модели. Понятие контроллеров Контроллеры являются "соединительной тканью " паттерна MVC, исполняя роль каналов между моделью данных и представлениями. Контроллеры определяют дейс­ твия. предоставляющие бизнес-логику, которая оперирует на модели данных и обес­ печивает представления данными, подлежащими отображению для пользователя. Контроллер, построенный с применением паттерна MVC, должен: • содержать действия. требующиеся для обновления модели на основе взаимо­ действия с пользователем. Контроллер не должен: • содержать логику, которая управляет внешним видом данных (это работа представления); • содержать логику, которая управляет постоянство м данных (это работа модели). Понятие представлений Представления содержат логику, которая требуется для отображения данных поль­ зователю или для сбора данных от пользователя. так что они могут быть обработаны каким-то де йствием контроллера. Представления должны: • содержать логику и разметку, необходимые для показа данных поль з ов ателю. Представления не должны: • содержать сложную логику (ее лучше поместить в контролл е р): • содержать логику. которая создает, сохраняет или манипулирует моделью пред­ метной области. Представления могут содержать логику, но она должна быть простой и использо­ ваться умеренно. Помещение в представление чего угодно кроме вызовов простейших методов или несложных выражений затрудняет тестирование и сопровождение при­ ложения в целом. Реализация MVC в ASP.NET Core Как подразумевает само название, реализация ASP.NET Core MVC адаптиру­ ет абстрактный патте рн MVC к ми ру разработки ASP.NET и С#. В инфраструктуре ASP.NET Core MVC контроллеры - это классы С#, обычно производные от класса Microsoft . AspNetCore . Mvc . Controll er. Каждый открытый метод в производном от Con t rol le r классе является методом действия. который ассоциирован с каким-то URL.
Глава 3. Паттерн MVC, проекты и соглашения 71 Когда запрос посьmается по URL, связанному с методом действия, операторы в данном ме­ тоде действия выполняются, чтобы провести некоторую операцию над моделью предмет­ ной области и затем выбрать представление для отображения клиенту. Взаимодействия между контроллером, моделью и представлением проиллюстрированы на рис. 3. 1 . НТТР Запрос ------------~ Ответ Представление .-1- - -- - -- -1 Модель Контроллер ._________. представления .__ ______ . Модель Рис. 3.1 . Взаимодействия в приложении MVC _• Постоянство (обычно с помощью реляционной - - базы данных) В инфраструктуре ASP.NET Core МVС применяется механизм визуализации, из­ вестный как Razor, который является компонентом, ответственным за обработку представления для генерации ответа браузеру. Представления Razor - это НТМL­ шаблоны, содержащие логику С#, которая используется для обработки данных мо­ дели с целью генерации динамического содержимого, реагирующего на изменения в модели. Работа Razor рассматривается в главе 5. Инфраструктура ASP.NET MVC не налагает никаких ограничений на реализацию модели предметной области. Вы можете создать модель с применением обычных объектов С# и реализовать постоянство с использованием любых баз данных, инф­ раструктур объектно-реляционного отображения или других инструментов работы с данными, поддерживаемых в .NET . Сравнение MVC с другими паттернами Разумеется, MVC- далеко не единственный архитектурный паттерн программно­ го обеспечения. Есть много других паттернов подобного рода, и некоторые из них являются или, по крайней мере, были чрезвычайно популярными. О паттерне MVC можно узнать много интересного, сравнивая его с альтернативами. В последующих разделах кратко описаны разные подходы к структурированию приложения и приве­ дено их сравнение с МVС . Одни паттерны будут близкими вариациями на тему MVC, в то время как другие - совершенно отличаться. Я не утверждаю, что МVС - идеальный паттерн во всех ситуациях. Я сторонник выбора такого подхода. .который лучше всего решает имеющуюся задачу. Как вы вскоре увидите, есть ситуации, когда не.которые конкурирующие паттерны в равной степени полезны или лучше MVC. Я призываю делать информированный и осознан­ ный выбор паттерна . Сам факт чтения вами этой книги наводит на мысль, что в опре­ деленной мере вы уже склонны применять паттерн MVC, но всегда полезно сохранять максимально широкую точку зрения. Паттерн ин теллектуального пользовательского интерфейса Один из наиболее распространенных паттернов проектирования известен как ин­ теллектуальный пользовательский интерфейс (Smart UI). Большинству программис­ тов приходилось создавать приложение с интеллектуальным пользовательским ин­ терфейсом на том или ином этапе своей профессиональной деятельности - меня это определенно касается. Если вы использовали Windows Forms или ASP.NET Web Forms, то сказанное относится и к вам.
72 Часть 1. Введение в инфраструктуру ASP.NET Core MVC При построении приложения с интеллектуальным пользовательским интерфейсом разработчики конструируют интерфейс, часто перетаскивая набор компонентов или элементов управления на поверхность проектирования или холст. Элементы управ­ ления сообщают о взаимодействии с пользователем, инициируя события для щелч­ ков на кнопках. нажатий клавиш на клавиатуре, перемещений курсора мыши и т.д. Разработчик добавляет код реакции на эти события в набор обработчиков событий. которые являются небольшими блоками кода, вызьmаемыми при выдаче специфичес­ кого события в определенном компоненте. В конечном итоге получается монолитное приложение, показанное на рис. 3.2 . Код. который поддерживает пользовательский интерфейс и реализует бизнес-логику. перемешан между собой без какого-либо раз­ деления обязанностей. Код, который определяет приемлемые значения для вводимых данных и запрашивает данные или модифицирует, например, расчетный счет поль­ зователя, оказывается размещенным в небольших фрагментах, связанных друг с дру­ гом в предполагаемом поряДitе поступления событий. Запрос ~ Интеллектуальный пользовательский Ответ интерфейс __ ._ Постоянство (обычно с помощью реляционной - - базы данных) Рис. 3.2 . Паттерн интеллектуального пользовательского интерфейса Интеллектуальные пользовательские интерфейсы идеальны для простых проектов, т.к. позволяют добиться неплохих результатов достаточно быстро (по сравнению с раз­ работкой MVC, которая, как показано в главе 8, требует определенных начальных за­ трат, прежде чем будут доставлены результаты). Интеллектуальные пользовательские интерфейсы также подходят для построения прототипов пользовательских интерфей­ сов. Их инструменты визуального конструирования могут оказаться по-настоящему удобными, и если вы обсуждаете с заказчиком требования к внешнему виду и потоку пользовательского интерфейса, то инструмент интеллектуального пользовательского интерфейса может быть быстрым и удобным способом для генерации и проверки раз­ личных идей. Самый крупный недостаток интеллектуальных пользовательских интерфейсов связан с тем, что их трудно сопровождать и расширять. Смешивание кода модели предметной области и бизнес-логики с кодом пользовательского интерфейса при­ водит к дублированию, когда один и тот же фрагмент бизнес-логики копируется и вставляется для поддержки вновь добавленного компонента. Нахождение всех дубли­ рованных фрагментов и применение к ним исправления может быть непростой за­ дачей. Добавление новой функции может оказаться практически невозможным без нарушения работы каких-то существующих функций. Тестирование приложения с интеллектуальным пользовательским интерфейсом также может быть затруднено . Единственный способ тестирования - эмуляция взаимодействия с пользователем , что является далеким от идеала и трудно реализуемым фундаментом для обеспечения полного покрытия тестами. В мире МVС интеллектуальный пользовательский интерфейс часто называют ан­ типаттерном, т.е. чем-то таким, чего следует избегать любой ценой . Эта антипатия возникает (по крайней мере, частично) оттого, что к инфраструктуре MVC обращают­ ся в поисках альтернативы после множества не слишком успешных попыток разра-
Глава 3. Паттерн MVC, проекты и соглашения 73 ботки и сопровождения приложений с интеллектуальным пользовательским интер­ фейсом , которые попросту вышли из-под контроля. Тем не менее, полный отказ от паттерна интеллектуального пользовательского интерфейса будет заблуждением . В нем не все настолько плохо, и с данным подхо­ дом связаны положительные аспекты. Приложения с интеллектуальным пользова­ тельским интерфейсом быстро и легко разрабатывать. Производители компонентов и инструментов проектирования приложили немало усилий, чтобы сделать процесс разработки приятным занятием, поэтому даже совершенно неопытный программист за считанные часы может получить профессионально выглядящий и достаточно фун­ кциональный результат. Наибольшая слабость приложений с интеллектуальным пользовательским интер­ фейсом - низкое удобство сопровождения - не проявляется при мелких объемах разработки. Если вы создаете несложный инструмент для небольшой аудитории, то приложение с интеллектуальным пользовательским интерфейсом может оказаться идеальным решением. Просто при этом не гарантируется увеличение сложности, под­ держиваемое приложением MVC. Архитектура "модель-представление" В приложении с интеллектуальным пользовательским интерфейсом проблемы со­ провождения обычно возникают в области бизнес-логики. которая настолько рассеяна по приложению, что внесение изменений или добавление новых функций становится мучительным процессом. Улучшить ситуацию помогает архитектура "модель-пред­ ставление'', которая выносит бизнес-логику в отдельную модель предметной облас­ ти. Все данные, процессы и правила концентрируются в одной части приложения (рис. 3.3). Запрос Ответ Пользовательский 1-------1~ интерфейс (представление) Модель _._ Постоянство (обычно с помощt реляционной - - базы данных) Рис. З.З. Архитектура "модель-представление" Архитектура "модель-представление'" может быть значительным усовершенствова­ нием по сравнению с монолитным интеллектуальным пользовательским интерфей­ сом (скажем, ее гораздо легче сопровождать). но возникают две проблемы. Первая: поскольку пользовательский интерфейс и модель предметной области тесно пере­ плетены, модульное тестирование любой из этих частей может оказаться затрудни ­ тельным . Вторая проблема проистекает из практики, а не из определения паттерна. Обычно модель содержит большой объем кода доступа к данным (не обязательно. но в большинстве случаев) , а это означает, что модель данных содержит не только бизнес ­ данные, операции и правила. Классическая трехуровневая архитектура Чтобы решить проблемы, присущие архитектуре "модель-представление'", трехзвен­ ная или трехуровневая архитектура отделяет код реализации постоянства от модели предметной области и помещает его в новый компонент, который называется уровн.ем доступа к данным (data access layer - DAL). Сказанное иллюстрируется на рис. 3.4.
74 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Запрос Ответ Пользовательский 1----.-i интерфейс (представление) Модель Рис. 3.4. Трехуровневая архитектура Уровень доступа к данным Постоянство - - •(обычно с помощью __ реляционной базы данных) Трехуровневая архитектура является наиболее широко используемым паттерном в бизнес-приложениях. Она не устанавливает никаких ограничений на способ реали­ зации пользовательского интерфейса и обеспечивает хорошее разделение обязаннос­ тей , не будучи излишне сложной. Кроме того, ценой некоторых усилий уровень DAL может быть создан так, чтобы модульное тестирование проводилось относительно легко. Можно заметить очевидные сходства между классическим трехуровневым при­ ложением и паттерном МVС. Разница в том, что когда уровень пользовательского ин­ терфейса напрямую связан с инфраструктурой графического пользовательского ин­ терфейса типа "щелчок-событие" (такой как Windows Forms или ASP.NET Web Forms), то выполнение автоматизированных модульных тестов становится практически не­ возможным. А поскольку часть пользовательского интерфейса трехуровневого при­ ложения может быть сложной, останется много кода, не подвергавшегося серьезному тестированию. В худшем сценарии отсутствие принудительных ограничений, накладываемых трехуровневой архитектурой на уровень поль зовательско го интерфейса, означает, что многие такие приложения в итоге оказываются тонко замаскированными при­ ложениями с интеллектуальным пользовательским интерфейсом без какого-либо ре­ ального разделения обязанностей. Это ведет к наихудшему конечному результату: не поддерживающему тестирование и трудному в сопровождении приложению. которое к тому же и чрезмерно сложно. Разновидности MVC Основные принципы проектирования приложений МVС были уже описаны, осо­ бенно те из них, которые применимы к реализации ASP.NET Core МVС. Другие реа­ лизации интерпретируют аспекты паттерна MVC иначе, дополняя, подстраивая или ка~<-то еще адаптируя его для соответствия области охвата и целям своих проектов . В последующих разделах мы кратко рассмотрим две наиболее распространенных ва­ риации на тему MVC. Понимание этих разновидностей не имеет особого значения в случае работы с ASP.NET Саге MVC. Данная информация включена ради полноты картины, потому что такие термины будут употребляться в большинстве обсуждений паттернов проектирования ПО . Паттерн "модель·nредставление-презентатор" Паттерн "модель-представление- презентатор" (model-view-presenter - МVР) яв­ ляется разновидностью МVС и разработан для того, чтобы облегчить согласование с поддерживающими состояние платформами графического пользовательского интер­ фейса, такими как Windows Forms или ASP.NEТ Web Forms. Это достойная по пытк а из­ влечь лучшее из паттерна интеллектуального пользовательского интерфейса, избежав проблем, которые он обычно привносит.
Глава 3. Паттерн MVC, проекты и соглашения 75 В этом паттерне презентатор имеет те же обязанности, что и контроллер МVС, но он также более непосредственно связан с представлением, сохраняющим информа­ цию о состоянии, напрямую управляя значениями, которые отображаются в компо­ нентах пользовательского интерфейса в соответствии с вводом и действиями пользо­ вателя. Существуют две реализации этого паттерна. • Реализация пассивного представления, в которой представление не содер­ жит никакой логики. Такое представление служит контейнером для элементов управления пользовательского интерфейса, которыми напрямую управляет презентатор . • Реализация координирующего контроллера, в которой представление может от­ вечать за определенные элементы логики презентации, такие как привязка дан­ ных, и получать ссылку на источник данных от моделей предметной области. Отличие между этими двумя подходами касается уровня интеллектуальности представления . В любом случае презентатор отделен от инфраструктуры графичес­ кого пользовательского интерфейса , что делает логику пр езентатора более простой и подходящей для модульного тестирования. Паттерн "модель-представление-модель представления" Паттерн "модель-представление-модель представления" (model-view-view model - МWМ) - это последняя разновидность МVС. Он появился в Microsoft и используется в инфраструктуре Windows Presentation Foundation (WPF). В паттерне МWМ модели и представления играют те же самые роли, что и в MVC. Разница связана с присутс­ твующей в МWМ концепцией модели представления, которая является абстрактным представлением пользовательского интерфейса. Как правило, модель представле­ ния - это класс С#, который открывает доступ к свойствам для данных, подлежащих отображению в пользовательском интерфейсе, и операциям с данными. инициируе­ мым из пользовательского интерфейса. В отличие от контроллера МVС модель пред­ ставления МWМ не имеет ни малейшего понятия о существовании представления (или любой конкретной технологии пользовательских интерфейсов). Представление МWМ применяет средство привязки WPF, чтобы установить двунаправленное соеди­ нение между свойствами , доступ к которым открывают элементы управления в пред­ ставлении (вроде пунктов раскрьmающегося меню или эффекта от щелчка на кнопке), и свойствами . доступ к которым открывает модель представления. На заметку! В MVC также используется термин модель представления, но он относится к простому классу модели, предназначенному только для передачи данных из контроллера в представление, как противоположность моделям предметной области, которые являют­ ся сложными представлениями данных, операций и правил. Проекты ASP.NET Core MVC При создании нового проекта ASP.NET Core МVС среда Visual Studio предлагает на выбор несколько вариантов начального содержимого, которое желательно иметь в проекте. Идея в том, чтобы облегчить разработчикам-новичкам процесс обучения и применить сберегающий время передовой опыт при описании распространенных средств и задач. Я не поклонник такого подхода в отношении заготовленных про­ ектов или нода. Намерения обычно хороши, но исполнение никогда не приводит в
76 Часть 1. Введение в инфраструктуру ASP.NET Core MVC восторг. Одной из характеристик, которая мне больше вс его нравит ся в ASP.NET и MVC, является высочайшая гибкость в подгонке платформы под мой стиль разработ­ ки. Процессы, классы и представления, которые создает и наполняет среда Visual Studio, вызывают у меня чувство , что я вынужден работать в стиле кого-то другого . К тому же я нахожу содержимо е и конфигурацию слишком обобщенными и прими­ тивными, чтобы приносить хоть какую-нибудь пользу. Разработчики в Microsoft не могут знать, какой вид приложения необходим, поэтому они охватьmают все основы, но настолько обобщенным путем. что в итоге я просто избавляюсь от всего стандар­ тного содержимого. Моя рекомендация (всем, кто по недоразумению спросил) заключается в том, что­ бы начинать с пустого проекта и добавлять нужные папки , файлы и пакеты. Вы не только узнаете больше о способе работы МVС, но и будете обладать полным контро­ лем над тем , что содержит ваше приложение . Но мои предпочтения н е должны формировать вашу практику разработки. Вы мо­ жете счесть шаблоны более удобными, чем я. особенно если явля етесь новичком в разработке ASP.NET и е ще не выработали стиль, который устраивает лично вас. Вы может е такж е признавать шаблоны проектов полезным ресурсом и источником идей, хотя должны проявлять осмотрительность и не добавлять функциональность к прило­ жению до того , как полностью поймете, каким образом она работает. Создание проекта Когда вы впервые создаете новый проект ASP.NET Core, то име ете три базовых от­ правных точки. из которых можно выбирать: шаблон Empty (Пустой), шаблон Web API и шаблон Web Application (Веб-приложение), как показано на рис. 3 .5 . Stftct а t empfote: r ------------- --------., 1 А project templ•I• for cre•t •ng an ASP.NEТ Coro ASP.NEТ Core Te mp lat es 1' / •ppltcat1on wrth "" ampfe ASP.NET MVC Vil!\., ; and Controllers. This ttmplate can af so Ье used for RESTTul I~~ij 1' НТТР servtces . 1 1 Empty Web API Web 1 1 Ш!!!.т.ш Applicatto n 1 l~c-h~an-g-eA_ut_h_e_.nt-ic-al-io~n 1 iL ______________________ 1 Authe:n ticotio n: No AuthentkatIOn <;!) Microso ft Azure G О Host in the cloud ~-5:_rvice --] ~OK~,, ~J Рис. 3.5. Шаблоны проектов ASP.NET Core Шаблон проекта Empty содержит связующие механизмы для инфраструктуры ASP.NET Core, но не включает библиот еки или конфигурацию, требующиеся прило­ жению МVС. В состав шаблона проекта Web API входят инфраструктуры ASP.NET Core и MVC, а также пример приложения. который демонстрирует получение и обработ ку
Глава 3. Паттерн MVC, проекты и соглашения 77 запросов Ajax от клиентов. Шаблон проекта Web Application содержит инфраструк­ туры ASP.NET Core и MVC с примером приложения, иллюстрирующим генерацию НТМL-содержимого. Шаблоны Web API и Web Application могут конфигурироваться с различными схемами для аутентификации пользователей и авторизации их доступа к приложению. Шаблоны проектов могут производить впечатление , что для создания определен­ ного вида приложения ASP.NET вы обязаны следовать специфическому пути, но это не так. Шаблоны - это просто разные отправные точки для получения той же самой функциональности, и вы можете добавлять в проекты, созданные с помощью любого шаблона , любую желаемую функциональность . Скажем, в главе 20 объясняется, как им еть дело с запросами Ajax, а в главах 28-30 - что делать с аутентификацией и ав­ торизацией, причем все примеры будут начинаться с шаблона проекта Empty. Таким образом, реальная разница между шаблонами проектов связана с началь­ ным набором библиотек, конфигурационных файлов, кода и содержимого, добавля­ емого средой Visual Studio при создании проекта. Существует много отличий между самым простым (Empty) и самым сложным (Web Application) проектами, как можно видеть на рис. 3.6, где показано окно Solution Explorer после создания проектов с ис­ пользованием каждого шаблона. Для случая с шаблоном Web Application пришлось привести снимки окна Solution Explorer с разными открытыми папками, потому что единственный список файлов не уместился на печатной странице. Дополнительные файлы, которые шаблон Web Application добавляет в проект, вы­ глядят устрашающе, но часть из них - всего лишь заполнители или реализации, ил­ люстрирующие общие функциональные возможности . .l - Solutionlttm: IJ tJ!r>b•l.J~Ofl }' PIOp('lties tJ t.\of\<hStШn91.j1on ~ -·1001 • • Otptnckntit:s Со Pн19rem .cs .l rJp1ojcrtJи111 n pro;нtJocli:.j~Qn . D P•ojt(t_Rt•dm~html С• St1r11tp.H у wtb.c~fig , 5:1. ..,, ,~ t> J- P109ff11ts •• Rtfrr tncн ." -V.~001 !:> • ·• DrprndtN:iti ..r .- Co nt10Jlt." с• Acc"untCont1olltr.cs t" HomeControllrцs с• М.n19rCon11ollcr.c~ • "" D1t1 • ....• м.g1111cns t> с- OOOOOOOOOOOOOO.Crt1trldf.nlit)·St С" Applк.11tionObCon!txtModf.!Srit с- Applic.t1ionDbContm.cs ь illl! t-.tud~lt • Strvicн Р l[m1i1Srndt1.o t • ISmsSt"dr:цs с• Meщ9tScnAct,.ci Jt 81 Vi~ lJ 1ppsrtting1j10" 61 Ьundlteonfig .jso n с- P1ogr1m.c1 " IJ project.j1or1 .! ~' Projr:cLR"dmc. html e< S1a rtup.cs f) wtt.conl19 • At<O unl @1 Cofll11mEm.11l.c\htmt !!!) (ictvnaltoginConfirm!bon.c1h!ml 8 Ь1:ttl\lllogin FJИu1c.cshtml ~for9ctPшwaid.cshtml В Forg otP1иword Co r1firmation .c1'11ml 8 Locl:out.,shtml В login.rshtml [!!) д.f9•Sttr.cshtml @) P.t u,1P1mwo1d.c.111tml 0 Rt~ttP,шwo1dConfirn1•tion.cshtml В SrndCodt.uhtml 8 'hrifyCodc.c1html • loo.· Homt /!) AЬouttthtml @j Cont1ct.0Nml G) lndu.cshlml ;..· M1"1gr @1 AddPhontN11mЬtt.cshtml 121 Ctш19rPt~nчo1d .<Ui1ml 0 lndu.иhl ml @lNorцgtlogin,.<thtml В SftP•1swo1d. нhtml 0 VtrifyPhc~umbtr.csh1ml • ....- Sh"rd е .tl)'O\ILt~htmt В .L oginP11rti•l.ohtm! В .V11l1d1t1onXript~Patti11Ltshtml [!) (rfor.cshtml @l .Viewimportм~htnil @} .ViewScer1.c1html с- b1(m1llog1nCo"Г. rmtti0nVir...Mod!!I.« с.• for9otP1s~rcМrwModtl.н <• loginVie1.~.'\odrl.cs с• Rrgist rt'lreY<Modd.c s с• Rt1t!Pauwo1dVitwМodrl.{~ t• !.tndCodrV-IC!'WМodrl .п с- \lf1ify(ode\'~Modt1.ts • ;,,..· M1rцg rV1rwModrh t • AddPhontNumbr!VitwModr Lu с- 01n9eP1нwo1dVlrwModrl .< 1 с• ConfigurrTwo F1ctoJV1e1.~ModrJ.c1 С" f1ctcrV1ewMQdrl.ci с- lnduVitwMode!.ts ео J. l..,..9d.vyi11}Vif.'WМ0Ucl.~} С* Rtm0Vtlog1nVi,. .. Modrl,CJ с• SdP•иwoнfVitwf"1odt~C1 С" Vt•ily?hontNumbr<V1ewModt!.c.t С" Applktlil)r!Ultf.U /> • ir"•9н /1$j' • .. .. r1ь " "1i boot1t1Jp !:> ~ j qutty 1> О jqUt"'f·v.tГod1li11n·unoЫru1N't !J _1dtrtncn.j~ f1viton .ico Рис. 3.6. Стандартное содержимое , добавляемое в проект шаблонами Empty и Web Application
78 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Одни дополнительные файлы настраивают MVC или конфигурируют ASP.NET . Другие представляют собой библиотеки клиентской стороны , которые будут вклю­ чаться в состав НТМL-разметки, генерируемой приложением. Список файлов в на­ стоящий момент может выглядеть огромным, но к концу книги вы будете понимать, что делает каждый из них . Независимо от того , какой шаблон применяется для создания проекта , существу­ ют общие папки и файлы, имеющиеся во всех шаблонах . Одни элементы в проекте играют специальные роли, которые жестко закодированы в инфраструктуре ASP.NET или МVС либо каком-то инструменте из числа поддерживаемых Visual Studio . Другие связаны с соглашениями об именовании, которые используются в большинстве про­ ектов ASP.NET или MVC. В табл. 3.1 описаны важные файлы и папки, с которыми вы столкнетесь в проекте ASP.NET Core MVC; часть из них не присутствует в проекте по умолчанию , но будет представлена в последующих главах. На заметку! Все папки и файлы, описанные в табл . 3.1, находятся в папке s r c , которая является местоположением, где Visual Studio создает проект ASP.NET Core MVC внутри решения . Таблица 3.1. Сводка по элементам проекта MVC Папка или файл /Area s /Dependencies /Compone nt s / Contro llers /Data /M igrat ions / Mo dels /Views Описание Области - это способ разбиения крупных приложений на мелкие порции. Области рассматриваются в главе 16 Элемент зависимостей предоставляет подробные сведения обо всех пакетах, от которых зависит проект. Диспетчеры пакетов , применяемые Visual Studio, описаны в главе 6 Здесь определены классы компонентов представлений, которые используются для отображения самодостаточных средств, таких как корзины для покупок. Компоненты пред­ ставлений обсуждаются в главе 22 Сюда помещаются классы контроллеров. Это соглаше- ние. Классы контроллеров могут размещаться где угодно, потому что все они компилируются в одну и ту же сборку. Контроллеры подробно рассматриваются в главе 17 Здесь определены классы контекста баз данных, хотя я предпочитаю игнорировать это соглашение и определять их в папке Mod e ls, как будет продемонстрировано в главе 8 Здесь хранятся детали схем баз данных, чтобы можно было обновлять развернутые базы данных. Процесс развертыва­ ния будет показан в главе 12 Сюда помещаются классы моделей представлений и моде­ лей предметной области. Это соглашение. Классы моделей могут определяться где угодно в текущем проекте или даже в отдельном проекте Здесь хранятся представления и частичные представления, обычно сгруппированные в папки с именами контроллеров, к которым они относятся. Представления будут подробно описаны в главе 21
Глава 3. Паттерн MVC , проекты и соглашения 79 Окончание табл. 3. 1 Папка или файл Описание /Views/Shared Здесь хранятся компоновки и представления , которые не являются специфическими для каких-то контроллеров . Представления будут подробно описаны в главе 21 /Views/ _Viewimport s . cshtml Этот файл применяется для указания пространств имен, которые будут включены в файлы представлений Razor, как объясняется в главе 5. Он также используется для установ­ ки дескрипторных вспомогательных классов (глава 23) /Views / ViewStart . cshtml Этот файл позволяет указать стандартную компоновку для механизма визуализации Razor, как описано в главе 5 /bower. j son По умолчанию этот файл скрыт. Он содержит список паке­ тов , управляемых диспетчером пакетов Bower (глава 6) /proj ect.j son В этом файле указаны базовые конфигурационные пара­ метры для проекта , в том числе применяемые им пакеты NuGet (глава 6) /Progr am . cs Этот класс конфигурирует платформу, на которой размеща­ ется приложение (глава 14) /Startup . cs Этот класс конфигурирует само приложение (глава 14) /wwwroot Сюда помещается статическое содержимое, такое как фай­ лы CSS и изображений. Кроме того, именно сюда диспет­ чер пакетов Bower устанавливает пакеты JavaScript и CSS (глава 6) Соглашения в проекте MVC В проекте MVC существуют два вида соглашений . Первый вид - это просто предпо­ ложения о то м , как про ект может быть структурирован. Например , пакеты JavaScript и CSS от независимых поставщиков, на которые вы полагаетесь , общепринято поме­ щать в папку www r oot/lib . Им енно там ожидают обнаружить их другие разработчш<И МVС , и сюда их будет устанавливать диспетчер пакетов. Но вы вольны п ер еименовать папку 1 ib или вообще удалить ее и хранить пакеты в другом месте. Это не помешает инфраструктуре MVC выполнить ваше приложение при условии, что элементы script и l i nk в представлениях ссылаются на местоположение , куда вы пом е стили пак еты. Второй вид соглашений вытекает из принципа соглашения по конфигурач,ии (или соглашения над конфигурач,ией, если делать акцент на пр еимуществе соглашения пе­ ред конфигурацией), который был одним из главных аспектов, обеспечивших попу­ лярность платформе Ruby оп Rails . Соглашение по конфигурации означает, что вы не должны явно конфигурировать, с1<ажем, ассоциации между контроллер ами и их пред­ ставлениями. Вы просто следуете определенному соглашению об именовании для сво­ их файлов - и все работает. Когда приходится иметь дело с соглашени ем такого вида, снижается гибкость в отношении изменения структуры проекта. В последующих раз­ делах объясняются соглашения, которые используются вместо конфигурации. Совет. Все соглашения могут быть изменены путем замены стандартных компонентов MVC собственными реализациями . В книге будут описаны различные способы делать это , что поможет объяснить, как работают приложения MVC, но с рассматриваемыми здесь согла­ шениями придется иметь дело в большинстве проектов.
80 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Следование соглашениям для классов контроллеров Классы контроллеров имеют имена, которые заканчиваются на Controller, на­ пример, ProductController, AdminController и HomeController. При ссылке на контроллер в другом месте проекта, скажем, в случае применения дескрипторного вспомогательного класса, указывается первая часть имени (такая как Product), а ин­ фраструктура MVC автоматически добавляет к этому имени слово Controller и на­ чинает поиск класса контроллера. Совет. Это поведение можно изменить, создав соглашение для моделей, как будет описано в главе 31. Следование соглашениям для представлений Представления и частичные представления помещаются в папку /Views/ ИмяКонт ­ роллера. Например, представление, ассоциированное с классом ProductController, должно находиться в папке /Views/Product. Совет. Обратите внимание, что часть Controller имени класса в имени папки внутри Views не указывается, т.е. используется /Views/Product, а не /Views/ProductController. Поначалу такой подход может показаться нелогичным, но он быстро войдет в привычку. Инфраструктура МVС ожидает, что стандартное представление для метода дейс­ твия должно иметь имя этого метода. Например, представление, ассоциированное с методом действия по имени List (),должно называться List . cshtrnl. Таким обра­ зом, ожидается, что для метода действия List () класса ProductController стан­ дартным представлением будет /Views/Product/List. cshtrnl. Стандартное пред­ ставление применяется при возвращении результата вызова метода View ( ) в методе действия, примерно так: return View() ; Можно указать имя другого представления: return View("MyOtherView"); Обратите внимание, что мы не включаем в представление расширение имени фай­ ла или путь. При поиске представления инфраструктура МVС просматривает папку, имеющую имя контроллера, а затем папку /Views/Shared. Это значит, что представ­ ления, которые будут использоваться более чем одним контроллером, можно помес­ тить в папку /Views/Shared и MVC успешно найдет их. Следование соглашениям для компоновок Соглашение об именовании для компоновок предусматривает снабжение имени файла префиксом в виде символа подчеркивания (_J и размещение файлов компо­ новки в папке /Views/Shared. Стандартная компоновка применяется по умолчанию ко всем представлениям через файл /Views/_ ViewStart . cshtrnl. Если вы не хотите, чтобы стандартная компоновка применялась к представлениям, то можете изменить
Глава 3. Паттерн MVC, nроекты и соглашения 81 настройки в файле _ViewStart.cshtml (либо вообще удалить его ) , указав другую компоновку в представлении, например: @{ Layout = " -1 _MyLayout. cshtml"; Или же можете отключить компоновку для заданного представления: @{ La yout = null; Резюме В этой главе был представлен архитектурный паттерн МVС и его сравнение с не­ Сl{Олькими другими паттернами, с которыми вы могли сталкиваться или слышать о них ранее. В следующей главе объясняется структура проектов MVC в Visual Studio и рассматриваются важнейшие средства языка С#, которые используются при разра­ ботке веб-приложений МVС.
ГЛАВА 4 Важные функциональные возможности языка С # в этой главе будут описаны функциональные возможности С#, используемые при разработке веб - приложений, 1юторые не очень хорошо понимают или часто путают. Однако настоящая книга не посвящена языку С#, так что для каждой воз ­ можности будет приводиться только краткий пример , поэтому вы можете прорабо ­ тать примеры из остальных глав книги и задействовать нужные возможности в своих проектах . В табл . 4.1 приведена сводка для данной главы. Таблица 4. 1 . Сводка по главе Задача Избегание обращения к свойствам по ссылкам nu ll Упрощение свойств С# Упрощение формирования строк Создание объекта и установка его свойств за один шаг Добавление функциональности в класс, который не может быть модифицирован Упрощение использования делегатов и однострочных методов Применение неявной типизации Создание объектов без определения типа Упрощение использования асинхронных методов Получение имени метода или свойства клас­ са без определения статической строки Решение Листинг Используйте nu ll-условную 4.6-4.9 операцию Используйте автоматически реа- 4.10-4 .12 лизуемые свойства Используйте интерполяцию строк 4.13 Используйте инициализатор 4.14-4 .17 объекта или коллекции Используйте расширяющий 4.18-4 .25 метод Используйте лямбда - выражение 4 .26-4 .33 Используйте ключевое слово var 4.34 Используйте анонимный тип 4.35, 4.36 Используйте ключевые слова 4.37-4.40 asyncи await Используйте выражение nameof 4.41, 4.42
Глава 4. Важные функциональные возмо ж ности языка С# 83 Подготовка проекта для примера Для этой главы создайте в Visual Studio новый проект по имени LanguageFeatur es , используя шаблон ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NЕТ Core)). Снимите отметку с флажка Add Application lnsights to Project (Добавить в проект службу Application Insights) и щелкните на кнопке ОК (рис. 4.1). ! !; Recent i ~-·!r" !f\rt.Jllr:d .с Templistes .с Visu1t!C• f itJindo~vs \ ~ Onliм wеь .N E1 Core And1oid Cloud Exten~ibllity iOS Reporting Silverlight ··1 - - ·- - " S.Ort Ьу: ~efau!t_ __ ij ASP.NП \Чf!Ь Applica tion (.NЕТ F ran\~ork} ASP.N ET Core V./eb Applicatio n (.N П Framtwoik) ." .:::: ". !~- ·~::J VisualC: VisualC~ -· -· - - ·- ~ ~,;_c!:'_J~sblled !:m!:!~ts.\~ tr/"fJ. Туре: Vi1ui1I с; Projec t t empl.ttes for cre11tin9 ASP.NET Core •pplic11 tions for Windo\>6, linux and 0) Х using .NП Core. ' ApplkottIOn lnsJ9hts О Add Applit~tion ln~ ight!; to proje<:t Optimize performonce •nd monitot usage in ycur live opplicaticn, Help ntt undtrs~nd Appl1cttio.n lr1s!ghts Pri11;,cy Stat~.ment 1 \ Br~:;:J 1 So lutionnгme Li11n9 u:g~Fe0Jtur~ ., _ _ C.reatedl1 eф;iryforsolutlon ' L~-'---·-·--"-·----~--~-~--_:_--~-----·~-~~~:~~~~-·-·~-Add:~"~j~~~::r J!__:~~ !j Locatiorn Рис. 4.1 . Выбор типа проекта Когда отобразятся различные конфигурации проекта ASP.NET, выберите шаблон Empty (Пустой), как показано на рис. 4.2 , и щелкните на кнопке ОК, чтобы создать проект. 1 1 1 1 1 ~ Microsoft Azu r~ 1 S•l«t• lemplate: 1 с - ~- ~--- -----~- -----------"'--- ----1 An empty p,oject template fo r cr9tin9 an ASP.NП Core j AS P.NEТ Core Tem piates 1 app!kэtion. This templi!!te does not have any content in ' 1 it. 11 ~ij 1~ 1 1 WebAPI \'leb ,.1 I Application 1 1 , 1 _________l jj l~~~~-~~~)-t~~:.~~~~ i__ ··-· -- ·- ·--··-~--~- -- ·--·-·~--·~ ----··----~-J ~~~~~~~~n-----·--- 10 ОHortinthecloud lfдрр S~~~--·:·: '-'--·---~·-·j 1 L DC1~ 1 --- -·-- ·-- ----- ---- ------ --- ·---··· ---------------- --- __________J Рис. 4.2 . Выбор начального содержимого проекта
84 Часть 1. Введени е в инфрастру кт уру ASP.NET Core MVC Включение ASP.NET Core MVC Шаблон проекта Empty создает проект, который содержит минимал ьную конфи­ гурацию ASP.NET Core без какой-либо поддержки MVC. Это значит, что содержимое­ заполнитель, добавляемое шаблоном Web Application (Веб-приложени е), отсутствует, но танже означает необходимость в выполнении ряда дополнительных шагов для включения МVС, чтобы заработали такие ср едства , как контроллеры и пр едставле­ ния . Здесь мы внесем изменения, требуемые для внлючения МVС в проекте, но по1<а не будем вдаваться в детали каждого шага . Первый шаг предусматривает добавле­ ние сборок .NET дл я МVС , что делается в разд еле dependencies файла proj ect . j son (листинг 4 . 1) . Листинг 4.1 . Добавление сборок MVC в файле proj ect. j son " dependencies ": { " Microsoft . NETCore . App" : "version ": 11 1.0 .0 11 , " type ": " platform" }1 }, " Microsoft . AspNetCore . Diagnostics ": "1. 0 .0", " Microsoft . AspNetCore . Server . IISintegration ": " 1 . 0 . 0 ", " Microsoft.AspNetCore . Server . Kestre l": " 1.0.О ", " Microsoft.Extensions . Logging . Console ": " 1.0.0 ", "Microsoft .AspNetCore .Mvc": "1.0.0" В разделе dependencies файла proj ect. j son перечислены сборки, которые тре­ буются для про екта. Мы добавили сборку Microsoft. AspNetCore. Mvc, содержащую классы МVС. Обратите внимание на добавление запятой в конце строки, предшес­ твующей Microsoft . AspNetCore . Mvc. Файлы конфигурации JSON чувствительны к корректному форматированию, а о добавлении запятой легко забыть и тем самым вызвать ошибку. Совет. Каждая сборка указывается с номером версии. Вы должны удостовериться , что все сборки с указанными версиями нормально работают вместе . Во время редактирования файла proj ect. j son среда Visual Studio предоставит списо к доступны х верси й сборок . Простейший подход заключается в том, чтобы указанная вами версия для Microsoft . AspNetCore . Mvc совпадала с версией существующих сборо к в разделе dependencies, который был добавлен Visual Studio , когда создавался проект. На следующем шаге инфраструктуре ASP.NET Core сообщается о необходимости использования MVC, что делается в классе Startup (листинг 4.2). Листинг 4.2 . Включение MVC в файле Startup . cs using Microsoft.AspNetCore . Builder; using Microsoft.AspNetCore . Hosting ; using Microsoft.AspNetCore . Http ; using Microsoft . Extensions.Dependencyinjection;
Глава 4. Важные функциональные возможности языка С# 85 using Mi crosoft . Extensions . Logg i ng ; narnespace LanguageFeatures { puЫic class Star tup { puЫic void Config u reServices (I Se r vi ceCollection services ) { services.AddМvc(); puЫic void Configure(IAppl i cationBu i l d e r ар р, I Host i ngEnvironrnen t env , ILogge r Factory loggerFact ory) { app.UseMvcWithDefaultRoute(); Конфигурировани е приложений ASP.NET Core МVС будет объясняться в главе 14, а пока достаточно знать, что два оператора, добавленные в листинге 4.2, обеспечивают б а зовую н а стройку MVC с примен ением стандартной конфигур ации и соглаш ений . Создание ком поненто в приложения MVC Имея настро енную инфраструктуру МVС, можно добавлять 1<0мпоненты приложения МVС , которые будут исполь зоваться для демонстрации важных языковых средств С# . Создание модели Н ачн ем с создания простого класса модели, чтобы иметь какие-то данные , с кото­ ры ми можно работать. Добавьте папку по имени Models и создайте в ней файл класса Product . cs с определением, прив еденным в листинге 4.3 . Листинг 4.3 . Содержимое файла Product. cs из папки Models narnespace LanguageFeatures . Models { puЬlic cla s s Product { puЫic string Narne { get ; set ; } puЫic decirnal? Price { get ; set ; puЬlic static Product[] GetP r oducts() Product kayak = new Pr oduct { Narne = "Kayak", Price = 275М }; Product l ifejacket = new Prod u ct Narne = " Lifejacket ", Price = 48 . 95М }; return new Product[] { kayak , lifejacket , n ull }; В ю~асс е Products определ е ны свойства Name и Pri c e, а также статич е ский метод по имени GetP r oducts () , который возвращает массив элементов Product . Один из эле ментов , соде ржащихся в возвращаемом из метода GetProducts () массив е, уста­ новлен в null.
86 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Создание контроллера и представления В примерах этой главы мы применяем простой контроллер для демонстрации ра з­ личных языковых средств . Создайте папку Con trollers и добавьт е в не е файл класса по имени HomeContro lle r . cs , содержимое которого показано в листинг е 4.4 . В слу­ чае использования стандартной конфигурации MVC инфраструктура будет по умол ­ чанию отправлять НТГР-запросы контроллеру Home . Листинг 4.4 . Содержимое файла HomeController . cs из папки Controllers u sing Microsoft . AspNetCore . Mvc ; n amespace Lang u age Feat u res . Contro lle rs puЬlic clas s HomeControl ler : Controll er puЬlic Vi e wRes u lt I ndex() return Vi ew(new string[] { "С#", "Language", "Feat ures" }); Метод действия Index ( ) сообщает инфраструктуре MVC о необходимости визуа ­ лизировать стандартное представление и передает ей массив строк, который должен быть включен в НТМL-разметку, отправляемую клиенту . Чтобы создать соответствую­ щее пр едставление, добавьте папку Vi ews/Home (сначала создав папку Views , а зат е м внутри нее паш<у Home ) и поместите в нее файл представления по имени Index . cshtml с содержимым, приведенным в листинге 4 .5 . Листинг 4.5 . Содержимое файла Index. cshtml из папки Views/Home @model I E n um eraЫe<string> @{ Layout = n ull; <!DOCTYPE html > <html> <head> <meta name= "viewport" content= "width=device-width" / > <t itle >Language Features</t itl e> </head> <body> <ul> @foreach (string s in Model ) { <li>@s</l i> </ul> </body> </html > Запустив пример приложения путем выбора пункта Start Debugging (Запустить от­ ладку) в меню Debug (Отладка) , вы увидите вывод , предст авленный на рис . 4.3.
Глава 4. Важные функциональные возможности языка С# 87 D langu age features Х ____ ? ~athost:sз._29_0___ с" .~ • La11guage • Feanю~s _J Рис. 4.3 . Запуск пример приложения Поскольку выводом во всех примерах в данной главе является текст, отображаемые браузером сообщения в дальнейшем будут показаны так: С# Language Features Использование null - условной оп е рац и и С помощью null -условной операции можно более элегантно обнаруживать зна­ чения null. Во вр емя разработки пр иложений МVС встр ечается много проверок на предмет null, т.к. нужно выяснять , содержит ли запрос специфический заголовок или значение либо содержит ли модель определенный элемент данных. Традиционно работа со значениями null требовала явных проверок, которы е могли становиться утомительны м и и подверженными ошибкам , когда требовалось инспектировать и объект, и его свойства. За счет применения null -условной операции такие проверки будут намного легч е и компактнее (листинг 4.6). Листинг 4.6 . Обнаружение значений null в файле HomeController. cs using Microsoft . AspNetCore . Mv c ; using System.Collections.Generic; using LanguageFeatures.Models; nam e space LanguageFeatures . Controlle rs puЫic class HomeController : Control ler puЬlic ViewResult Index() { List<string> results = new List<string>(); foreach (Product р in Product.GetProducts()) string name = p?.Name; decimal? price = p?.Price; results .Add (string. Format ("Name: {0}, Price: {1}", name, price)); return View(results);
88 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Статический метод GetProducts (), определенный в классе Product, возвраща­ ет массив объектов, который инспектируется в методе действия Index () контрол­ лера с целью получения списка значений Name и Price. Проблема в том, что как объект в массиве, так и значения его свойств могут быть null, т.е. нельзя прос­ то ссылаться нар. Narne или р. Price внутри цикла foreach, не получив исключе­ ние NullReferenceException. Во избежание этого используется null-условная операция: string name = p?.Name; decimal? price = p?.Price; null-условная операция обозначается знаком вопроса(?). Если значение р равно null, то переменная name также будет установлена в null. Если значение р не равно null, то переменной name будет присвоено значение свойства Person. Name . Такой же проверне подвергается свойство Price. Обратите внимание. что переменная. ноторой выполняется присваивание с применением null-условной операции, должна быть в состоянии иметь дело со значениями null, поэтому переменная price объявлена с десятичным типом, допускающим null (decimal ?). Связывание в цепочки null-условных операций Для навигации по иерархии объектов null-условные операции могут связывать­ ся в цепочки и превращаться в по-настоящему эффективный инструмент для упро­ щения нода и обеспечения безопасной навигации. В листинге 4. 7 I{ классу Product добавлено свойство с вложенными ссылками, что создает более сложную иерархию объектов. Листинг 4. 7. Добавление свойства в файле Product. cs namespace Language Feat ures.Models { puЬlic class Product { puЬlic string Narne { get; set ; } puЫic decimal? Price { g et ; set ; puЫic Product Related { get; set; } puЫic stati c Product[J GetProducts() Product kayak = new Product { Name = " Kayak" , Price = 275М }; Product lifejacket = new Product Name = " Lifejacket" , Price = 48 . 95М }; kayak.Related = lifejacket; return new Product[] ( kayak, lifejacket, null }; Каждый объект Product имеет свойство Related, которое может ссылаться на дру­ гой объект Product . В методе GetProducts () мы устанавливаем свойство Related
Глава 4. Ва ж ные фун к циональные возмо ж ности языка С# 89 для объекта Produ c t , представляющего каяк . В листинге 4.8 показано, как можно со единить вместе nu ll-условные опер ации для навигации по свойствам объектов. не вызывая исключение . Листинг 4.8 . Обнаружение вложенных значений null в файле HomeController. cs using Microsoft . AspN e tCore . Mv c; using System. Collecti ons . Gener ic; using LanguageFeatures . Models ; namespace Language Featu r es . Co n t r o ll ers puЫic cl ass HomeCont roller : Cont roller puЫic ViewResul t Inde x() { List<string> resu l ts = new List<st r ing>() ; foreach (P r oduct р in Product . GetProducts()) string name = p? .Name; deci mal? price = p? . Price; string relatedName = p?.Related?.Name; results .Add (string. Format ( "Name: {О}, Price: {1}, Related: {2}", name, price, relatedName)); r etur n Vi ew(results) ; n ull-условную операцию можно применять :к каждой части цепочки свойств, например: string relatedName = p? .Related?. Na me; В таком случа е переменная re latedName получит значение n u ll, когда значение null и м еет р или р . Rela ted. Иначе relatedNam e будет присвоено знач ение свойства р . Rela ted . Name . Запустив пример прилож ения, вы увидите в окне браузера следую­ щий вывод: Name: Kayak, Pri ce: 275, Rel ated: Lifejacket Name: Lifejacket, Pri ce: 48.95 , Related: Name: , Price: , Related: Ко мби ниро в ание null-условной операции и операции объединения с null Для установки альтернативного зн ачения, представляющего n ull, удобно комби­ ниров ать nu l l -условную оп е рацию (один знак вопроса) и операцию объ един е ния с nu l l (два знака вопроса), как демонстрируется в листинге 4.9. Листинг 4.9 . Сочетание операций работы с null в файле HomeController. cs using Microsoft . As pNetCore . Mvc ; using System.Coll ections.Generic; using LanguageFeatu r es . Models ;
90 Часть 1. Введение в инфраструктуру ASP.NET Core MVC namespace LanguageFeatures.Controllers { puЬl i c class HomeController : Con troller puЫic ViewResult Index() { List<string> results = new List<string>(); foreach (Product р in Product . GetProducts( ) ) string name = р? .Name ?? "<No Name>"; decimal? price = p? . Price ?? О; string relatedName = р? .Related? .Name ?? "<None>"; results.Add(string . Format( " Name: (0 ), Price : (1) , Related : {2} ", name , price , relatedName)) ; return View(results); null-условная операция гарантирует, что при навигации по свойствам объектов не возникнет исключение Nul lReferenceExcep tion , а операция объединения с null обеспечивает отсутствие значений null в результатах, отображаемых в браузере. Если вы запустите пример приложения, то увидите в окне браузера следующий вывод: Name : Kayak , Price : 275 , Related : Lifejacket Name : Lifejacket, Price : 48 . 95 , Re l ated : <None> Name: <No Name>, Price : О, Related : <None> Использование автоматически реализуемых свойств В языке С# поддерживаются автоматически реализуемые свойства, которые мы применяли при определении свойств класса Person в предыдущем разделе: namespace LanguageFeatures .M odels { puЫic class Product { puЫic string Name { get; set; } puЫic decimal? Price { get; set; puЬlic Product Related { get; set; } puЬlic static Product(] GetProducts() Product kayak = new Product { Name = " Kayak ", Price = 275М }; Product lifejacket = new Product Name = " Lifejacket ", Price = 48 . 95М }; kayak . Related = lifejacket ; return new Product[J { kayak , lifejacket , nul l };
Глава 4. Важные функциональные возможности языка С# 91 Данное средство дает возможность определять свойства . не реали зуя блоки кода get и set. Средство автоматически реализуемых свойств позволяет трактовать сле­ дующее опр еделение свойства: puЫic string Name { get ; set; } как экви в ал ентное приведенному ниже коду: puЫic st r ing Name { get { return name ; set {name =value; } Ср едст в а подобного типа и з вестны как "синтаксический сахар", который делает р аботу с С# более приятной (за счет устранения в этом случа е избыточного кода , дуб­ л ируем ого в итоге для 1шждого свойства), однако существенно не изм еняет поведение яз ы ка. Термин " сахар" может показаться уничижит ельным, но любые улучшения. ко ­ торые облегчают написание и сопровождение кода . приносят пользу - особенно в крупных и сл ожных про ектах. И спользо ва ни е и ни циал и заторов автомат и чески реализуемых свойст в Автоматич е ски р е ализуе мые свойства поддерживаются, н ачиная с ве р сии С # 3.0. В последней версии С# доступны инициализаторы для автоматич ески реализуемых свойств . которые позволяют устанавливать начальны е значения. не требуя примене­ ния конструкторов (листинг 4 . 10) . Листинг 4.1 О. Использование инициализатора автоматически реализуемого свойства в файле Product. cs n a mespace LanguageFeatures . Models { puЫic c l ass Product { puЫic string Name { get; s et; } puЬlic string Category { get; set; puЬlic decimal? Pri ce { get; set; } puЫi c Product Related { get ; set ; } puЫic stati c Product[ J GetP r oducts ( ) Product kayak = new Product { Name = "Kayak" , Category = "Water Craft", Price = 275М }; Product l i fejacke t = new Product { Name = "Li fejacket", Price= 48.95М }; ka y ak . Re l ated = lifejacket; "Watersports"; return new Produc t [] { kayak , life jacket , null } ;
92 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Присваивание значения автоматически реализуемому свойству не препятствует применению установщика для изменения свойства в более позднее время, а всего лишь приводит в порядок код для простых типов с конструкторами. которые содер­ жат список присваиваний свойств, обеспечивающих наличие стандартных значений. В приведенном примере инициализатор присваивает свойству Category значение 11 Wa tersports 11 • Начальное значение может быть изменено, что и делается при со­ здании объекта k ayak . когда ему указывается значение 11 Water Craft 11 • Создание автоматически реализуемых свойств только для чтения Можно создать свойство только для чтения, используя инициализатор и опуская ключевое слово set из определения автоматически реализуемого свойства, 1юторое имеет инициализатор (листинг 4.11). Листинг 4.11. Создание свойства только для чтения в файле Product. cs namespace LanguageFeatures.Models { puЬlic class Product { puЫic string Name { get ; set ; } puЫ i c string Category { g et; set; puЫic decimal? Price { get ; set ; } puЬlic Product Rel ated { get; set; } puЬlic bool InStock { get; } = true; puЫic static Product [ J GetProducts() Product kayak = new Product Name = " Kayak 11 , }; Category = 11 Wate r Craft ", Price = 275М Product l ifejacket = new Product { Name = 11 Li fe j acket 11 , Price = 48. 95М }; kayak.Related = l ifejacket ; 11 Watersports" ; return new Product[] { kayak , l ifejacket , null } ; Свойство I nS tock инициализируется значением true и не может быть изменено; тем не менее, ему можно присвоить значение внутри конструктора типа, кmt показа­ но в листинге 4 . 12. Листинг 4.12. Присваивание значения свойству только для чтения в файле Product. cs namespace LanguageFeatures . Models { puЫic class Product ( puЫic Product(bool stock = true) InStock = stock;
Глава 4. Важ ные функциональные возмо ж ности языка С# 93 puЬl ic string Name { get; set; } puЫic string Category { ge t; set ; puЫic decimal ? Price { get; set; } puЫic Product Related { ge t; s et ; } puЫic Ьооl InStock { get; } puЫic st atic Pr od uct[] GetProduct s(} Product kayak = new Product { Name = " Kayak ", }; Category = " Water Craft ", Pri ce = 275М "Water spor ts"; Product lifejacket = new Product (false) Name = "Lifejacket", Price = 48.95М }; kayak . Re l ated = l ifejacket ; return new Product[J { kayak, lifejacket, null }; Конструктор позволяет указывать в 1шчестве аргумента значение для свойства, допускающего только чтение, и если значение не пр едоставлено, тогда он устанавли­ вает свойство в стандартное значение tru e . Посл е установки конструктором значе­ ния свойства оно не может быть изменено. Исп ользова ние интерполяции строк Традиционным инструментом С# для образования строк, содержащих значения данных, является метод string. F o r mat ( }. Вот пример применения этого приема в контроллер е Horne : resul ts . Add ( string. Format ( "Name: {О}, Price: { 1}, Related: { 2}", name, price, relatedName) ) ; В С# 6.0 появилась поддержка другого подхода , называемого интерполяцией строк, который позволяет избежать необходимости гарантировать, что ссылки {О} в шаб­ лоне строки соответствуют переменным, указанным в качестве аргументов. Взамен интерполяция строк использует имена переменных напрямую (листинг 4.13). Листинг 4.1 З. Применение интерполяции строк в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System.Coll ections.Generic; using LanguageFeat ur es .M o d e ls; namespace LanguageFeat u res . Controllers puЫic class HomeContro l ler : Contro ll er puЬli c ViewRe s ult Index() { List<string> r e sults = new List<s t ring>();
94 Часть 1. Введение в инфрастру к туру ASP.NET Core MVC foreach (Product р in Product . GetProducts()) string name = p? .Name ?? " <No Name> "; decimal? price = p? . Price ?? О; string relatedName = p? . Related? . Name ?? " <None>"; results . Add($"Name : {name}, Price: {price}, Related: {relatedName}"); return View(results) ; Интерполируемая строка снабжается префиксом в виде символа $ и содержит "дыры", которые представляют собой ссылки на значения, содержащиеся внутри фи­ гурных скобок ( { и }). Когда строка оценивается, "дыры" заполняются текущими зна­ чениями указанных переменных и констант. Среда Visual Studio обеспечивает поддержку средства IntelliSense для создания интерполированных строк и предлагает список доступных членов, когда набирается символ {; это помогает минимизировать количество опечаток, а результатом оказы­ вается формат строки, который легче понять. Совет. Интерполяция строк поддерживает все спецификаторы формата, которые доступ­ ны для метода string . Format (} . Спецификаторы формата включаются в виде части "дыры", поэтому $ " Pri ce : {price : С2 }" сформатирует значение price как дене ж ное значение с двумя десятичными цифрами . Использование инициализаторов объектов и коллекций При создании объекта в статическом методе GetProducts () класса Product при­ менялся иниц,иализатор объекта. который позволяет создавать объект и указывать значения его свойств за один шаг, например: Product kayak = new Product Name = "Kayak", Category = "Water Craft", Price = 275М }; Это еще одна форма "синтаксического сахара", делающая язык С# легче в исполь­ зовании. Без такого средства пришлось бы вызывать конструктор Product и затем применять вновь созданный объект для установки всех его свойств: Product kayak = new Product() ; kayak.Name = " Kayak "; kayak . Category = " Water Craft "; kayak.Price = 275М ;
Глава 4. Ва ж ные функциональные возмож ности язы ка С# 95 Свя з анное с ним сред ство - ин.ич,иализатор колле кции - позволя ет создавать колл е1щию и указ ывать ее содержимое за один шаг. Без такого инициализатора со­ здани е , к примеру. м ассива потребовало бы указания размера и элементов массива по отдельности, как показ ано в листи н ге 4.14 . Листинг 4 .14. Инициализация массива в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System . Co l lections . Gener i c ; using LanguageFeatures . Models ; namespace LanguageFeature s. Control l ers puЫic class HomeController : Controlle r puЫic Vi ewResult Index() { string[] names = new string[З]; names [О] "ВоЬ"; names[1] = "Joe" ; names [2] = "Alice"; return View("Index", names); Инициализатор коллекции дает возм ожность ука зать содержимое массива как часть е го конструирования , что неявно предоставля ет компилятору ра зме р м ассива (листинг 4.15). Листинг 4.15. Использование инициализатора коллекции в файле HomeController. cs using Microsoft . Asp NetCo re. Mvc ; using System.Col lections . Ge neric ; us i ng LanguageFeatures . Mod el s ; namespace LanguageFeature s. Control l ers puЫic class HomeController : Controller puЬlic ViewResult Inde x () { return View("Index", new string[J { "ВоЬ", "Joe", "Alice" }) ; Эл ементы массива задаются м ежду символами { и }, что позволяет получить более кратко е определ е ние коллекции и делает возможным определять ко ллекцию внутри вызова метода. Код в листинге 4.15 дает тот же результю~ что и код в листинге 4.14, и если вы запустит е пример приложения, то получите в окне браузера следующий вывод : ВоЬ Joe Alice
96 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Использование инициализатора индексированной коллекции В С# 6 улучшен способ применения инициализаторов коллекций для создания кол­ лекций , использующих индексы, таких как словари. В листинг е 4.16 приведен код метода действия Index (), переписанный для определения коллекции с помощью под­ хода С# 5 к инициализации словаря. Листинг 4.16. Инициализация словаря в файле HomeController. cs using Microsoft .A spNetCore . Mvc ; using System . Col l ections . Generic ; using LanguageFeatures . Models ; n amespace LanguageFeatures . Cont rollers puЫic class HomeController : Controller puЫic ViewResult Index() { Dictionary<string, Product> products = new Dictionary<string, Product> { { "Kayak", new Product { Name = "Kayak", Price = 275М } } , { "Lifejacket", new Product{ Name = "Lifejacket", Price = 48. 95М } }; return View("Index", products.Keys); Синтаксис для инициализации такого типа коллекции слишком сильно полагается на символы { и }, особенно когда значения коллекции создаются с применением иници­ ализаторов объектов. В С# 6 предлагается более естественный подход к инициализации индексированной коллекции, который согласован со способом извлечения или модифи­ кации значений после того, как коллекция была инициализирована (листинг 4.17). Листинг 4.17 . Использование синтаксиса инициализатора коллекции С# 6 в файле HomeController. cs using Microsoft.AspNetCore.Mvc; using System . Co llections.Generic; u sing LanguageFeatures . Models ; n amespace LanguageFeatures.Controll ers puЬlic c l ass HomeController : Controlle r puЫ i c ViewResult Index() { Dictionary<string, Product> products = new Di ctionary<string , Product> { [ "Kayak"] = new Product { Name = "Kayak", Price = 275М } , [ "Lifejacket"] = new Product { Name = "Lifejacket", Price = 48. 95М }; return View( " Index ", products.Ke ys) ; Результат остается прежним , т.е. создание словаря, ключами ноторого являются Kayak и Lifejacket, а значениями - объекты Product, но элементы создаются с применением системы обозначений для индексов, используемой в других операциях
Глава 4. Важные функциональные возможности языка С# 97 с коллекциями. Запустив пример приложения, вы увидите в окне браузера следую­ щий вьmод: Kayak Lifejacket Использование расширяющих методов Расширяющие методы - это удобный способ добавления методов в 1Uiaccы, вла­ дельцем 1<оторых вы не являетесь и не можете модифицировать напрямую. В листин­ ге 4.18 приведено определение 1Uiacca Sh oppingCart , добавленного в папку Models в виде файла ShoppingCart . cs, который представляет коллекцию объектов Product. Листинг 4.18. Содержимое файла ShoppingCart. cs из папки Мodels using System . Co llections.Generic ; n a mespace LanguageFeatures . Models puЫic class ShoppingCart { puЫic IEnumeraЬle<P r od uc t> Products { get ; set; } Это простой класс, который действует в качестве оболоч1ш для коллекции List объектов Product (в данном примере необходим лишь эл е ментарный класс). Предположим , что нам требуется возможность определения общей стоимости объек­ тов Product в классе ShoppingCart , но мы не можем изменить сам класс, возможно потому, что он поступил от третьей стороны, и мы не располагаем его исходным ко­ дом. Для добавления нужной функциональности можно применить расширяющий ме­ тод . В листинге 4.19 показан класс MyExtensionМethods, также добавленный в папку Models в виде файла MyExtensionМet hods . cs. Листинг 4.19. Содержимое файла МyExtensionМethods. cs из папки Models namespace LanguageFeatures . Mode l s { puЫic static c l ass MyExtens i o nMethods { puЫic static decimal Tota lPr ices(this ShoppingCart cartParam) { decimal total = О; foreach (Product prod in cartParam . Products) total += prod?.Price ? ? О ; return total ; Ключ е во е слово this, расположенное перед первым параметром, помечает TotalPrices () ка~< расширяющий метод. Первый параметр указывает .NЕТ, к какому классу может применяться расширяющий метод - I< ShoppingCart в данном случае. На экземпляр класса ShoppingCart, к которому применен расширяющий метод. мож­ но ссылаться с использованием параметра cartParam. Расширяющий метод прохо­ дит по объектам Product в ShoppingCa r t и возвращает сумму значений их свойств Product . Price. В листинге 4.20 демонстрируется применение расширяющего метода в методе действия контроллера Horne.
98 Часть 1. Введение в инфраструктуру ASP.NET Core MVC На заметку! Расширяющие методы не позволяют нарушать правила доступа, которые клас­ сы определяют для своих методов , полей и свойств. С помощью расширяющего метода функциональность класса можно расширить, но с использованием только тех членов, к которым в любом случае имеется доступ. Листинг 4.20. Применение расширяющего метода в файле HomeController. cs u sing Micr oso ft . AspNetCore . Mvc ; using Syst e m . Collections . Generic ; using Language F eatures . Mod e ls ; namespace LanguageFeatures . Control l ers puЫic clas s HomeController : Co n tro l l e r puЫic ViewResult I ndex() { ShoppingCart cart = new ShoppingCart { Products = Product.GetProducts() }; decimal cartTotal = cart.TotalPrices(); return View (" Index", new string [] { $" Total: { cartTotal: С2}" } ) ; Вот ключевой оператор: d ecimal cartTotal = cart . Tota l Prices() ; Метод TotalPrices () вызывается на объект е ShoppingCart, как е сли бы он был частью класса ShoppingCart , хотя он является расширяющим методом, которы й оп­ ределен в совершенно другом класс е . Среда .NET будет обнаруживать расширяющи е классы , если они находятся в области действия текущего класса , т. е. являются частью того же самого пространства имен или пространства имен, которо е указано в опера­ тор е using. Запустив пример приложения, вы увидите в окне брауз е ра следующий вывод: Total : $323 . 95 Применение рас ш иря ющих методов к интерфейсу Можно такж е создавать расширяющие методы, которые применяются к интерфей­ су , что позволит вы з ывать таки е расширяющие методы для вс ех кл ас с ов , р е ализую­ щих этот интерф ейс . В листинге 4.21 приведен код класса ShoppingCart, модифици­ рованный для реализации интерфейса I Enume raЫ e<P r oduct>. Листинг 4.21 . Реализация интерфейса в файле ShoppingCart. cs using System.Collections; using System . Collection s.Generic ; namespace LanguageFeatur e s . Models puЬlic class ShoppingCart : IEnumeraЫe<Product> puЫic IEnumeraЫe<Product> Pr oducts { get; set;
Глава 4. Важные функциональные возможности языка С# 99 puЫic IEnumerator<Product> GetEnumerator () return Products.GetEnumerator(); IEnumerator IEnumeraЬle.GetEnumerator() return GetEnumerator(); Теперь расширяющий метод можно изменить так, чтобы он работал с интерфей­ со м IEnumeraЫe<Pro du ct> (листинг 4.22). Листинг 4.22. Модификация расширяющего метода в файле МyExtensionМethods. cs using System.Collections.Generic; namespac e Language Fea tures . Mo de l s puЫic static cl a s s MyExt ensi onMethods puЫic static decimal TotalPrices (this IEnumeraЫe<Product> products) decimal total = О ; for each (Product prod in produc ts) total += prod?.Price ?? О; return total ; тип первого параметра был изменен на I EnumeraЫe<Pro du ct>, а это значит, что цикл fo r e a ch в теле метода работает непосредственно с объектами Prod uct . Переход на использование упомянутого интерфейса означает, что мы можем подсчитать об­ щую стоимость объектов Produ ct , перечисляемых посредством любого интерфейса IEnumeraЫe<Produc t > . что включает не только экз емпляры Shoppi ngCart , но также массивы объектов Produ c t (листин г 4.23) . Листинг 4.23. Применение расширяющего метода к массиву в файле HomeController. cs using Microsoft . As p NetCore . Mvc ; using System . Collections . Generic ; using LanguageFeatures . Mode l s ; namespace LanguageFe atures . Co ntro ll ers puЫic cl ass HomeControll e r : Controller puЫic ViewResu l t Index() { ShoppingCart cart = new ShoppingCart { Products = Product . GetPr oducts() }; Product [] productArray = { new Product {Name = "Kayak", Price = 275М}, new Product {Name = "Lifejacket", Price = 48.95М} }; decimal cartTotal = cart.TotalPrices(); decimal arrayTotal = productArray.TotalPrices();
100 Часть 1. Введение в инфраструктуру ASP.NET Core MVC return View("Index", new string[] $ "Cart Total: { cartTotal: С2} " , $ "Array Total: { arrayTotal: С2}" } ) ; Если вы запустите проект, то увидите показанные ниже результаты, которые де­ монстрируют. что расширяющий метод возвращает один и тот же результат, незави­ симо от способа перебора объектов Product: Cart Total : $323 . 95 Array Total: $323 . 95 Создание фильтрующих расширяющих методов Последний аспект расширяющих методов, о котором необходимо упомянуть - возможность их использования для фильтрации коллекций объектов. Расширяющий метод, который оперирует на интерфейсе IEnumeraЫe<T> и также возвраща ет IEnumeraЫe<T>. может задействовать ключевое слово yield, чтобы применить кри­ терий отбора к эл ементам в источнике данных с целью ген е рации сокращенного на­ бора результатов. В листинге 4.24 представлен такой метод, который добавляется в класс MyExtensionMethods . Листинг 4.24 . Добавление фильтрующего расширяющего метода в файле МyExtensionМethods . cs using System . Collections . Generic ; namespace LanguageFeatures.Models { puЫic static class MyExtensionMethods puЫic static decimal TotalPrices(this IEnumeraЫe<Product> products) { decimal total = О; foreach (Product prod in products) total += prod?.Price ?? О ; return total ; puЫic static IEnumeraЫe<Product> FilterByPrice( this IEnumeraЫe<Product> productEnum, decimal minimumPrice) foreach (Product prod in productEnum) { if ( (prod?. Price ?? О) >= minimumPrice) { yield return prod; } Расширяющи й метод по имени FilterByPrice () принимает дополнительный параметр. который позволяет фильтровать товары, так что в результате возвраща­ ются объекты Product, значение свойства Price которых совпадает или превыша­ ет значение, указанное в параметре. Использование этого метода демонстрируется в листинге 4.25.
Глава 4. Важные функциональные возможности языка С# 101 Листинг 4.25. Применение фильтрующего расширяющего метода в файле HomeController. cs using Microsoft . AspNetCore.Mvc; using Sys tern . Collections . Generic ; using LanguageFeatures . Models ; narnespace LanguageFeatures . Controllers puЫic class HorneController : Controller puЫic ViewResult Index() { Product[J productArray = { new Product {Narne "K aya k", Price = 275М} , new Product {Narne "L ifejacket ", Price = 48 . 95М), new Product {Name = "Soccer ball", Price = 19.SOM}, new Product {N ame = "Corner flag", Price = 34. 95М} }; decimal arrayTotal = productArray.FilterByPrice(20) .TotalPrices(); return View("Index", new string[] { $ 11 Array Total: {arrayTotal:C2}" }) ; При вызове метода FilterByPrice () на массиве объектов Product метод TotalPrices () получает и использует для подсчета суммы только те из них, которые стоят больше $20. Запустив пример приложения, вы увидите в окне браузера следу­ ющий вьmод: Total : $358 .90 Использование лямбда-выражений Лямбда-выражения - это средство, которое служит причиной многочисленных заблуждений и не в последнюю очередь из-за того, что упрощаемое им средство само вызывает путаницу. Чтобы понять решаемую задачу, рассмотрим расширяющий ме­ тод FilterByPrice (), который был определен в предыдущем разделе. Метод реали­ зован так, что он может фильтровать объекты Product по цене, а это значит. что если вы захотите фильтровать объекты по названию, то вам придется создать второй ме­ тод наподобие приведенного в листинге 4.26. Листинг 4.26. Добавление фильтрующего метода в файле МyExtensionМethods . cs using Systern.Collections.Generic; namespace LanguageFeatures . Models puЫic static class MyExtensionMethods puЫic static decirnal TotalPrices(this IEnurneraЫe<Product> products) { decirnal total = О ; foreach (Product prod in products) total += prod?.Price ?? О ; return total ;
102 Часть 1. Введение в инфраструктуру ASP. NET Соге MVC puЫic static IEnumeraЫe<Product> Fi l te r ByPrice( this IEn umeraЬle<Pro d uct> productEnum , decimal minimumPrice) { foreach (P r oduct prod in p r oductEnum) { if ((prod?.Price ?? 0) >= minimumPrice) { yield return prod ; puЫic s tatic IEnumeraЫe<Product> FilterByName ( this IEnumeraЫe<Product> productEnum, char firstLetter) { foreach (Product prod in productEnum) { if (prod?.Name?[O] == firstLetter) { yield return prod; В листинге 4.27 демонстрируется прим енение в контролл ере обоих фильтрующих мет одов для с оздания двух ра зных итоговых сум м . Листинг 4.27. Использование двух фильтрующих методов в файле HomeController. cs using Mi crosoft . AspNetCore . Mvc ; using System . Col l ections . Generic ; using LanguageFeat u res . Mode l s ; namespace LanguageFeatures . Cont rollers puЫic clas s HomeControl ler : Co n troller puЫ i c ViewResult Index() { Produc t[] pr oductArray = { }; new Product {Name " Kayak", Price = 275М} , new Prod u ct {Name " Lifejacket", Price = 48 . 95М} , new Product {Name "Soccerball", Price 19.50М), new Product {Name " Corner flag ", Price = 34 . 95М} decirnal priceFilterTotal = productArray.FilterByPrice(20) .TotalPrices(); decimal nameFilterTotal = productArray.FilterByName('S') .TotalPrices(); return View( 11 Index 11 , new string[] { $ 11 Price Total: {price FilterTotal: С2} 11 , $ 11 Name Total: {nameFil terTotal: С2} " }) ; Первый фильтр отбирает все товары с ценой $20 и выше, а второй фильтр - то­ вары с названиями, начинающимися на букву S. После запуска примера приложения вы увидите в окне браузера следующий вывод : Price Total : $358 . 90 Name Total : $19 . 50
Глава 4. Ва ж ные функциональные возмо ж ности языка С# 103 Определ ение функций Описанный выше процесс можно повторять до бесконечности и создавать филь­ трующие м етоды для каждого интересующего свойства и сочетания свойств. Более элегантный подход предусматривает отделение кода, обраб атывающего перечисле­ ние, от критерия отбора. В С# это делается легко, поскольку функции разрешено пе­ р едавать нак объекты . В листинге 4.28 показан единственный расширяющий метод, 1юторый фильтрует перечисление объектов Product , но делегирует отдельной функ­ ции решени е о том, каки е из них должны быть включ е ны в р езультат. Листинг 4.28. Создание универсального фильтрующего метода в файле МyExtensionМethods . cs using System . Collections . Generic ; using System; namespace LanguageFeatures . Mode ls puЫic static class MyExtensionMe t hods puЫic static decimal TotalP r ices(th is IEnumeraЫe<Product> products) decimal total = О; foreach (Product prod i n p r oducts) total += prod? . Price ?? О; }. return total; puЬlic static IEnumeraЫe<Product> Filter( this IEnumeraЬle<Product> productEnum, Func<Product, bool> selector) { foreach (Product prod in productEnum) if (selector(prod)) { yield return prod; Вторым аргументом метода Fil ter () является функция, которая принимает объ­ ект Product и возвращает значение типа boo l. М етод Filter () вызывает эту фун­ кцию для каждого объекта Product и включает его в результат. если функция воз­ вращает true . Для применения метода Filter () можно указать метод или создать автономную функцию (листинг 4.29). Листинг 4.29. Использование функции для фильтрации объектов Product в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; us i ng System . Collections .G e neric; using LanguageFeatures . Models ; using System; namespace Language Featu res . Contro l lers puЫic class HomeController : Controller
104 Часть 1. Введение в инфраструктуру ASP.NET Core MVC bool FilterByPrice(Product р) { return (p?.Price ?? 0) >= 20; puЫic ViewResult Index() { Prod uct [ ] productArray = { }; new Product {Name " Kayak", Price = 275М} , new Product {Name " Lifejacket", Price = 48 .95М} , new Product {Name " Soccer ball" , Price 19.SOM} , new Product {Name " Corner flag", P rice = 34 . 95М} Func<Product, bool> nameFilter = delegate (Product prod) { return prod?. Name? [О] == 'S' ; }; decirnal priceFilterTotal = productArray .Fi lter(FilterByPrice) . TotalPrices () ; decirnal nameFilterTotal = productArray .F i lter(narneFilter) . TotalPrices О ; return View( " Index ", new string [] { $ "P rice To ta l: {priceFi l terTota l: C2} ", $ " Name Total : {nameFilterTotal : C2} " }) ; Ни один из подходов не идеален. Определение методов вроде FilterByPrice () засоряет определение класса. Создание объекта Func<Product, boo l > устраняет данную проблему. но сопряжено с неудобным синтаксисом, который труден для вос ­ приятия и сопрово:нщения. Именно эту задачу решают лямбда-выраж ения, позволяя определять функции более элегантным и выразительным способом, как показано в листинге 4.30 . Листинг 4.30. Использование лямбда-выражений в файле HomeController. cs using Microsoft.AspNetCore .Mvc; using System . Collections .Generic ; using Languag eFeat ures.Models; using System ; namespace Lang u ageFeatures .Co nt rol ler s puЫic c l ass HomeContro l ler : Con t rolle r puЫic ViewResult Index() { Product [] productArray = { 1; new Product {Name " Kayak", Price = 275М} , new Product {Name " Lifej acket", Price = 48 . 95М} , new Product {Name " Soccer ball ", Price 1 9.50М} , new Product {Na me " Corner flag", Price = 34 .95М} dec i mal priceFilterTotal = productArray . Filter(p => (p? . Price ?? О) >= 20) . Tota lP rices(} ;
Глава 4. Важ ные функциональные возмо ж ности языка С# 105 decimal nameFilterTotal = productArray .Filter(p=>p?.Name?[O] == 'S') . TotalPrices(); return View( " Index ", new string[] $"Price Total: {priceFilterTotal : C2} ", $ " Name Total : {nameFilterTotal : C2}" }) ; Лямбда-выражения выделены полужирным. Пар аметры выражаются без указания типа, который будет выведен автоматически. Символы => можно читать как "направ­ ляется в" и связывают параметр с результатом лямбда-выражения. В рассматривае­ мом примере параметр Product по имени р направляется в результат bool, который будет равен true, если значение свойства Price эквивалентно или превышает 20 в первом выражении или если значение свойства Name начинается с буквы S во втором выражении. Этот код работает тем же самым образом, что и отдельный метод и деле­ гат в виде функции, но он короче и для большинства людей легче в восприятии. Другие формы лямбда-выражений Представлять логику делегата посредством лямбда-выражения вовсе не обязательно . С тем же успехом можно вызвать метод, подобный следующему: prod => EvaluateProduct(prod) Если требуется лямбда - выражение для делегата, который имеет несколько параметров, то параметры должны быть заключены в круглые скобки: (prod, count) => prod . Price > 20 && count >О И, наконец, если в лямбда-выражении необходима логика, которая требует более одного оператора, то ее можно реализовать, используя фигурные скобки ( { } ) и завершая блок опе­ ратором return: (prod , count) => // ... несколько операторов кода ... return r esult ; Вы не обязаны применять лямбда-выражения в коде, но они служат изящным способом вы­ ра же ния сложных функций в читабельной и ясной манере. Вы будете часто встречать лямб ­ да-выражения в примерах, приводимых в этой книге. Использование методов и свойств в форме лямбда-выражений В С# 6 поддержка лямбда-выражений была расширена так, что их можно при­ менять для реализации методов и свойств. Во время разработки приложений MVC, особенно при написании контроллеров, часто появляются методы, которые содержат единственный оператор, выбирающий данные для отображения и пр едставление для визуализации. В листинге 4.31 приведен код метода действия Index (), переписан­ ный в соответствии с таким общим шаблоном.
106 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 4.31. Создание общего шаблона действия в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System . Col l ections . Generic ; using LanguageFeatures . Models ; using System; using System.Linq; namespace LanguageFeatures.Controllers puЫic c l ass HomeController : Controlle r puЬlic ViewResult Index() { return View(Product.GetProducts() .Select(p => p?.Name)); Метод действия Index () получает от статического метода Product. GetProducts () коллекцию объектов Product и с помощью LINQ строит проекцию значений свойств Name. Затем эта проекция применяется в качестве модели представления для стан­ дартного представления. Запустив пример приложения, вы увидите в окне браузера следующий вывод: Kayak Lifejacket В окне браузера также будет присутствовать пустой элемент списка, потому что метод GetProducts () включает в свой результат ссылку null, но в настоящем разде ­ ле главы это неважно. Когда тело метода состоит из единственного оператора, его можно переписать в виде лямбда-выражения (листинг 4.32). Листинг 4.32. Представление метода действия как лямбда-выражения в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using System . Collect ions.Generic ; using Lang uage Features.Model s ; using System; using System . Linq ; namespace LanguageFeatures.Control l ers puЫic class HomeController : Controller puЫic ViewResul t Index () => View{Product.GetProducts{) .Select{p => p?.Name)); Лямбда-выражения для методов позволяют опустить ключевое слово return и ис­ пользуют символы = > (направляется в) для связывания сигнатуры метода (включ ая аргум енты) с его реализацией. Метод Index (), показанный в листинге 4.32, работает точно так же, как метод Index () из листинга 4.31, но выражается более лаконично. Тот же самый базовый подход можно также применять для определения свойств . В листинге 4.33 демонстрируется добавление в класс Product свойства, которое ис­ пользует лямбда-выражение.
Глава 4. Ва ж ные функциональные возмо ж ности языка С# 107 Листинг 4.33 . Представление свойства как лямбда-выражения в файле Product. cs namespace LanguageFeatures.Models { puЫic class Product { puЫic Product(bool stock = true) InStock = stock ; puЫic string Name { get ; set ; } puЫic string Category { get ; set; " Watersports "; puЫic decimal ? Price { get; set; } puЬlic Product Related { get ; set; } puЫic bool InStock { get; } puЫic bool NameBeginsWi thS => Name? [О] 'S' ; puЬlic static Product[] GetProducts() { Product kayak = new Product { Name = " Kayak ", }; Category = " Water Craft ", Price = 27 5М Product lifejacket = new Product(false) { Name = " Lifejacket ", Price = 48.95М }; kayak . Related = lifejacket ; return new Product[ ] { kayak, lifejacket, null ); Использование автоматического выведения типа и анонимных типов Ключевое слово var языка С# позволяет определять локальную переменную без явного указания ее типа, как показано в листинге 4.34. Такой прием называется выведением типа или неявной типизацией. Листинг 4.34. Использование выведения типа в файле HomeController. cs using Microsoft.AspNetCore . Mvc ; using System . Col l ections . Generic ; using LanguageFeatures . Mode ls; using System ; using System.Linq; namespace LanguageFeatures.Controllers puЫic class HomeContro l ler : Controller puЫic ViewResult Index() { var names = new [] { "Kayak", "Lifejacket", "Soccer ball" } ; return View(names);
108 Часть 1. Введение в инфраструктуру ASP. NET Core MVC Речь идет вовсе не о том, что переменная myVar iaЫe не имеет типа; мы всего лишь предложили компилятору самостоятельно вывести тип из кода. Компилятор ис­ следует объявление массива и решает, что он является строковым. Выполнение при­ мера дает следующий вывод: Kayak Li fejacket Soccer ball Использование анонимных типов Комбинируя инициализаторы объектов и выведение типов, можно создавать про­ стые объекты модели представления, которые удобны для передачи данных между контроллером и представл ением. без необходимости в определении класса или струк­ туры (листинг 4.35). Листинг 4.35 . Создание анонимного типа в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System . Co l lect i ons.Generic ; using LanguageFea tures . Models ; u sing System ; us ing System.Linq ; namespa ce LanguageFeatures.Controllers puЫic class HomeC ontroller : Contr ol l e r p u Ыic ViewResult Index() { var products = new [] { new { Name = "Kayak", Price = 275М } , new { Name = "Lifejacket", Price = 48. 95М } , new { Name = "Soccer Ьall", Price = 19.SOM}, new { Name = "Corner flag", Price = 34. 95М } }; return View(products.Select(p => p.Name)); Каждый объект в массиве product s относится к анонимному типу. Это не значит. что объекты являются динамическими в том смысле, в каком считаются динамически­ ми переменные JavaScript. Это просто означает, что определени е типа будет создано ав­ томатически компилятором. Строгая типизация по-прежнему обеспечивается. Скажем. вы можете получать и устанавливать только те свойства, которые были определены в инициализаторе. Запустив пример. вы увидите в окне браузера следующий вывод: Kayak Lifejacket Soccer ball Corner f lag Компилятор С# генерирует класс на основе имени и типов параметров в инициа­ лизаторе. Два анонимно типизированных объекта, которые имеют те же самые имена и типы свойств, будут относиться к одному и тому же автоматически сгенерированно­ му классу. В результате все объекты в массиве products получат один и тот же тип, поскольку они определяют те же самые свойства.
Глава 4. Ва ж ные функциональные возмо ж ности языка С# 109 Совет. Для определения массива анонимно типизированных объектов должно применять­ ся ключевое слово var , т.к. тип не будет создан до тех пор , пока код не скомпилирует­ ся , и его имя не известно. Все элементы в массиве анонимно типизированных объектов обязаны определять те же самые свойства , иначе компилятор не сможет выяснить тип массива . В целях демонстрации изменим вывод в листинге 4.36, чтобы отображать имя типа вместо значения свой ства Name. Листинг 4.36 . Отображение имени анонимного типа в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System . Col lections.Generic; using LanguageFeatures . Models ; using System ; using System.Linq ; namespace LanguageFeatures . Controllers puЫic class HomeController : Contro l ler puЬlic ViewResult Index() { var products = new [] { }; new { Name " Kayak", Price = 275М }, new { Name " Lifejacket ", Price = 48.95М } , new { Name " Soccer ball ", Price 19.50М }, new ( Name "Corner flag", Price = 34 .95М } return View(products.Select(p => p.GetType() .Name)); Всем объектам в массиве был назначен один и тот же тип, что можно увидеть , запустив пример. Имя типа не выглядит дружественным к пользователю, но оно и не рассчитано на непосредственное исполь з ование, к тому же в вашем случае может оказ аться другим: <>f~AnonymousType0'2 <>f ~A nonymousType0 ' 2 <>f ~AnonymousType0 ' 2 <>f~AnonymousTy pe0' 2 Использование асинхронных методов Одни м из недавних крупных добавлений к языку С# явля ется улучшение спосо­ ба работы с асинхронными методами. Асинхронные методы осуществляют возврат и выполняют работу в фоновом режиме с уведомлением о ее завершении, позволяя коду в это время заниматься другими действиями. Асинхронные методы - важный инс­ трумент при устранении узких мест в коде; они позволяют приложениям извл екать преимущества от наличия нескольких процессоров и процессорных ядер. выполняя работу в параллельном режиме. В инфраструктуре МVС асинхронные методы могут применяться для увеличения общ ей прои зв одительности приложения, предоставляя серверу большую гибкость от-
11 О Часть 1. Введение в инфраструктуру ASP.NET Core MVC носительно того, как запросы планируются и выполняются. Для вьmолн ения работы асинхронным образом используются два ключевых слова С# - async и await. Для целей данного раздела в пример проекта понадобится добавить новую сбор1\у .N ET . чтобы можно было делать асинхронные НТТР-запросы. В листинге 4.37 показа­ но добавление, произведенное в разделе dependenci es файла proj ect. j son. Листинг 4.37. Добавление ссылки на сборку в файле project. json "dependencies ": }' " Microsoft . NETCore.App" : "ver sion": "1.О.0", " type" : "platform" }' "M icrosoft . AspNetCore . Diagnostics ": " 1. О. О", "Mi crosoft .AspNetCore.Serve r.I ISintegrat ion": "1 . 0 . О ", "Mi crosoft.AspNetCore .Server.Kestrel ": "1. 0 . 0 ", " Microsoft.Extens i ons . Logg ing .C onso l e ": "1. 0.О ", "M icrosoft.AspNetCore . Mvc ": "1 . 0 .О", "System.Net.Http": "4 .1. 0" После сохранения файла pro j ect. j son среда Visual Studio загрузит сборку System. Net. Http и добавит ее в проект. В главе 6 процесс будет описан более подробно. Работа с задачами напрямую Язык С# и платформа .NET предлагают великолепную поддержку для асинхрон­ ных методов, но код быстро становится многословным, а разработчики , не привык­ шие к параллельному программированию , зачастую не могут справиться с необыч­ ным синтаксисом. В качестве примера в листинге 4.38 приведен метод по имени GetPageLength (), который определен в классе MyAsyncMethods, добавленном в пап­ ку Mode ls в виде файла MyAsyncMethods . cs . Листинг 4.38 . Содержимое файла МyAsyncМethods. cs из папки Мodels using System . Net .H ttp ; using System . Threading.Tasks; namespace LanguageFeatures.Models puЫic class MyAsyncMethods { puЫic static Task<long?> GetPageLength() HttpClient client = new HttpClient() ; va r httpTask = client.GetAsync("http://apress.com"); 11 Во время выполнения НТТР-запроса //можно было бы делать другую работу . return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => { return antecedent . Resu l t . Content . Headers.ContentLength; });
Глава 4. Важные функциональные возможности языка С# 111 Метод использует объект System. Ne t . Ht tp. HttpC lie nt для запрашивания со­ держимого домашней страницы издательства Apress и возвращает его длину. Работа, которая будет выполняться асинхронно , представлена в .NET как объект Ta sk . Объ е кты Task строго типизируются на основе результата , выдаваемого фоновой ра­ ботой . Таким образом , при вызове метода HttpClient . GetAs y n c () получается объ­ ект Task<HttpResponseMe s sage> . Он сообщает о том, что запрос будет выполнен в фоновом режиме и его р езультатом будет объект HttpRespons eM e ssage. Совет. Когда мы употребляем понятия вроде фоновый режим, то опускаем массу деталей, чтобы подчеркнуть только ключевые аспекты, которые важны для мира MVC . Поддержка . NE T для асинхронных методов и параллельного программирования в целом превосход­ на, и ее рекомендуется внимательно изучить, если вы хотите создавать по-настоящему высокопроизводительные приложения, которые могут извлекать преимущества от много­ процессорного или многоядерного оборудования . Вы увидите , как MVC облегчает созда­ ние асинхронных веб-приложений далее в книге по мере представления разнообразных функциональных средств. Самой непонятной для большинства программистов частью является продолже­ ние , пр едставляющее собой механизм , с помощью которого указыва ется то, что долж­ но произойти, когда фоновая задача завершится . В приведенном примере применяет­ ся метод Con tinueWi th () для обработки объекта HttpResponseMe s sag e , получаемого из метода Ht t pC lient . GetAsync () . Это делается с использованием лямбда-выраже­ ния , которо е возвращает значение свойства, содержащего длину полученного от веб­ се рвера Apress содержимого. Вот код продолжения : return httpTask . ContinueWith((Task<HttpRespon s eMessag e > anteceden t ) => { return antecedent .Result .Content .Headers . ContentLengt h; }); Обратите внимание , что ключевое слово r e turn встречается два раза. Именно эта часть вызывает путаницу . Первое применение ключевого слова return указыва­ ет, что возвращается объект Ta s k<Http Re spons eMessage> , который при завершении задачи возвратит (второе ключевое слово r e t u r n ) длину из заголовка Co nte n t Le ngth. Заголовок Con tentLength возвраща ет р е зультат long ? (тип long , допускающий зна­ чения nul l), т.е . результатом метода Ge tPage Length () является Ta s k< l ong?> : puЫic static Task<long?> GetPageLength() { Не переживайте , если все сказанное выглядит для вас бессмысленным - в этом вы не одиноки. Как раз по такой причине в Microsoft и решили добавить в С# ключевые сло в а , упрощающи е р а боту с асинхронными методами. Применение ключевых сло в async и await Разработчики из Microsoft ввели в язык С# два ключевых слова, которые специаль­ но призваны облегчить использование асинхронных методов, подобных HttpClient . GetAsync () . Ими являются async и a wa i t ; в листинге 4.39 показано, как с их помо­ щью упростить наш пример метода.
112 Часть 1. Введение в инфраструктуру ASP.NE T Core MVC Листинг 4.39. Применение ключевых слов async и await в файле МyAsyncМethods. cs using System.Net .H ttp; using System.Threading.Tasks; namespace LanguageFeatures.Models puЫic class MyAsyncMethods { puЫic async static Task<1ong?> GetPageLength() HttpClient client = new HttpClient(); var httpMessage = await client .G etAsync("http://apress.com"); return httpMessage.Content.Headers.ContentLength; Ключевое слово awai t используется при вызове асинхронного метода. Оно сооб­ щает компилятору С# о том, что необходимо подождать результата Task, который возвращается методом GetAsync (), и затем заняться выполнением остальных опе­ раторов в том же методе. Применение ключевого слова awai t означает. что мы можем трактовать результат метода GetAsync (), как если бы он был обычным методом, и просто присвоить воз­ вращаемый им объект HttpResponseMessage какой-нибудь переменной. Еще лучше то, что затем можно использовать ключевое слово return традиционным образом для выдачи результата из другого метода - значения свойства ContentLength в рассмат­ риваемом случае. Это намного более естественный подход, к тому же нам не придется беспокоиться по поводу метода ContinueWith () и многократного при менения юпоче­ вого слова return. При использовании ключевого слова await потребуется также добавить к сигнатуре метода ключевое слово async, как бьmо сделано в рассмотренном выше примере. Тип результата метода не изменяется - метод GetPageLength () по-прежнему возвращает Task<long?>. Причина в том, что ключевые слова await и async реализованы с при­ менением ряда искусных трюков компилятора, которые позволяют использовать более естественный синтаксис, но не изменяют того, что происходит внутри методов, к кото­ рым они применены. Программист, вызывающий метод GetPageLength () , по-прежне­ му имеет дело с результатом Task<long?>, т.к. все еще существует фоновая операция, которая выдает значение long, допускающее null; хотя, конечно же, программист мо­ жет также предпочесть пользоваться ключевыми словами awai t и async. Такой шаблон повсеместно соблюдается в контроллере MVC, что позволяет легко писать асинхронные методы действий (листинг 4.40). Листинг 4.40. Определение асинхронных методов действий в файле HomeController. cs using Microsoft . AspNetCore.Mvc; using System.Collections . Generic ; using LanguageFeatures.Models; using System; using System.Linq; using System.Threading.Tasks; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller
Глава 4. Важные функциональные возможности языка С# 113 puЬlic async Task<ViewResult> Index () { long? length = await MyAsyncMethods.GetPageLength(); return View(new string[] { $ 11 Length: {length}" }) ; Тип результата метода действия Index () изменен на Task<ViewResul t>. Это со­ общает MVC о том, что метод действия будет возвращать объект Tas k, который по завершении выдаст объект ViewResul t, а тот предоставит детали представления, подлежащего визуализации, и требующиеся ему данные . К определению метода было добавлено ключевое слово async, что позволило использовать 1wючевое слово awai t при вызове метода MyAsyncMethods. Ge tPathLength (). О продолжениях позаботят­ ся инфраструктура MVC и платформа .NET, а результатом будет код, который легко писать, читать и сопровождать. Запустив приложение, вы увидите вывод, похожий на показанный ниже (хотя наверняка с отличающейся длиной, т.к. содержимое веб­ сайта Apress часто изменяется): Length : 62164 Получение имен Во время разработки веб-приложений есть много задач, в которых необходимо ссылаться на имя аргумента , переменной, метода или класса. Распространенными примерами являются ситуации с генерированием исключения или созданием ошибки проверки достоверности при обработке пользовательского ввода. Традиционный под­ ход предполагал применение строкового значения с жестко закодированным именем (листинг 4.4 1). Листинг 4.41. Использование жестко закодированного имени в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using System . Collections.Generic; using LanguageFeat ur es . Mo del s ; using System ; using System.L inq ; namespace LanguageFeatures . Controllers puЫic class HomeController : Co n troller puЬlic ViewRes ul t I ndex() { var products = new [] { new { Name = "Kayak", Price = 275М } , new { Name = "Lifejacket", Price = 48. 95М } , new { Name = "Soccerball", Price = 19.SOM }, new { Name = "Corner flag", Price = 34. 95М } }; return View(products. Select(p => $"Name: {p.Name}, Price: {р. Price} ")); Вызов метода Select ( ) из LINQ генерирует последовательность строк, каждая из которых содержит жестко закодированную ссылку на свойства Name и Price. Запуск приложения дает следующий вьmод в окне браузера:
114 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Name: Kayak , Price: 275 Name: Lifejacket , Price: 48.95 Name: Soccer ball, Price: 19 . 50 Name: Corner flag, Price : 34 . 95 Проблема этого подхода в том, что он предрасположен к ошибкам, которые обус­ ловлены либо неправильно набранным именем, либо некорректно обновленным име­ нем в строке после рефакторинга. Результат может вводить в заблуждение , что осо­ бенно проблематично для сообщений, отображаемых пользователю. В С# 6 появилось выражение nameof, благодаря которому ответственность за формирование строки имени возлагается на компилятор (листинг 4.42) . Листинг 4.42 . Использование выражений nameof в файле HomeController. cs using Microsoft.AspNetCore.Mvc; using System . Collections . Generic ; using LanguageFeatures . Models ; using System ; using System . Linq ; namespace LanguageFeatures.Control lers puЬlic class HomeC ontroller Contro l ler puЫic ViewResult Index() { var products = new [] { }; new { Name " Kayak ", Price = 275М ), new { Name "Lif ejacket ", Price = 48.95М } , new { Name "Soccer ball" , Price 19. SOM }, new { Name "C orner flag ", Price = 34.95М ) return View(products.Select(p => $" {nameof (p.Name)}: {p.Name}, {nameof (р. Price)}: {р. Price} ")); Компилятор обрабатывает ссылку наподобие р . Name таким образом, что только последняя часть включается в строку, порождая тот же самый вывод, как и в преды­ дущем примере . Среда Visual Studio располагает поддержкой средства IntelliSense для выражений nameof , поэтому вы будете снабжены подсказками при выборе ссылок, а выражения корректно обновятся в случае рефакторинга кода. Поскольку за работу с nameof отвечает компилятор, применение недопустимой ссылки приводит к ошибке на этапе компиляции , что не позволит некорректным или устаревшим ссылкам ус­ кользнуть от глаз. Резюме В настоящей главе был дан обзор основных языковых средств С#, которые дол­ жен знать результативный программист приложений MVC. Язык С# является гибким в достаточной мере для того , чтобы обычно существовало несколько способов р еше­ ния любой задачи, но есть средства, с которыми вы будете чаще всего встречаться во время разработки веб-приложений и видеть повсюду в при ме рах , рассматриваемых в данной книге. В следующей главе будет представлен механизм визуализации Razor и приведены объяснения , как его использовать для генерации динамического содержи­ мого в веб-приложениях MVC.
ГЛАВА 5 Работа с Razor в приложении ASP.NET Core MVC для выпуска содержимого, отправляемого кли­ ентам , используется компонент, который называется мехш-шзмом визуалu­ за4uu. Стандартным механизмом визуализации является Razor, и он обрабатывает аннотированные НТМL-файлы, производя поиск инструкций, которые вставляют ди­ намическое содержимое в вывод, отправляемый браузеру. В этой главе дается краткое введение в синтаксис Razor, так что вы сможете опоз­ нать выражения Razor, когда столкнетесь с ними. Мы не собираемся превращать гла­ ву в исчерпывающий справочник по Razor; считайте ее в большей степени ускорен­ ным курсом по синтаксису. Особенности Razor будут раскрыты в последующих главах книги при рассмотрении других средств МVС. В табл. 5.1 приведена сводка , позволя­ ющая поместить Razor в контекст. Таблица 5.1. Помещение Razor в контекст Вопрос Что это такое? Чем он полезен? Как он используется? Существуют ли какие­ то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменился ли он по сравнению с версией MVC5? Ответ Razor - это механизм визуализации, отвечающий за встраивание данных в документы HTML Возможность динамической генерации содержимого являет- ся неотъемлемой частью процесса написания веб-приложения . Механизм визуализации Razor предоставляет средства, которые упрощают работу с остальной инфраструктурой ASP.NET Core MVC с применением операторов С# Выражения Razor добавляются к статической НТМL-разметке в файлах представлений. Эти выражения оцениваются для генерации ответов на клиентские запросы Выражения Razor могут содержать почти любые операторы С#. Иногда может быть трудно решить, должна ли логика принадлежать представ­ лению или контроллеру, что способно разрушить принцип разделения обязанностей, который является центральным в паттерне MVC В главе 21 объясняется , как написать собственный механизм визу­ ализации . Доступны механизмы визуализации от независимых пос­ тавщиков, но они обычно полезны только в ограниченных ситуациях и для них не гарантируется долгосрочная подцержка Механизм визуализации Razor работает в значительной степени таким же образом, как и в MVC 5, но с рядом удобных улучшений. Файл импортирования представлений используется для указания пространств имен, в которых будет производиться поиск типов при обработке представления , а также для определения мест, где нахо­ дятся дескрипторные вспомогательные классы (глава 23)
116 Часть 1. Введение в инфраструктуру ASP.NET Core MVC В табл. 5.2 приведена сводка по главе. Таблица 5.2 . Сводка по главе Задача Решение Доступ к модели представления Используйте выражение @Model для опре­ деления типа модели и выражение @rnodel для доступа к объекту модели Использование имен типов без Создайте файл импортирования их уточнения с помощью про­ странств имен Определение содержимого, которое будет использоваться множеством представлений Указание стандартной компоновки представлений Применяйте компоновку Используйте файл запуска представления Передача данных из контролле- Применяйте объект ViewBag ра представлению за предела­ ми модели представления Выборочная генерация содержимого Генерация содер жимого для кажд ого элемента в массиве или коллекции Используйте условные выражения Razor Применяйте выражение @foreach механизма Razor Подготовка проекта для примера Листинг 5.6, 5.15, 5.18 5.7, 5.8 5.9 -5 .11 5.12-5.14 5.16,5.17 5.19, 5 .20 5.21, 5 .22 Для демонстрации работы механизма визуализации Razor мы создали проект ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)) по имени Razor, используя шаблон Empty (Пустой), как делали это в предыдушей гла­ ве . Отредактировав раздел dependencies файла proj ect . j son, мы добавили сборку МVС (листинг 5.1). Листинг 5.1 . Добавление сборки MVC в файле proj ect. j son 11 dependenci е s 11 : { }, 11 Microsoft . NETCore.App 11 : 11 version 11 : 11 1.0.0 11 , 11 type 11 : 11 platform11 }, 11 Mi crosoft . AspNetCore.Diagnostics 11 : 11 1.0.0 11 , 11 Microsoft .AspNetCore.Server.IISintegration 11 : 11 1.0 .0 11 , 11 Microsoft.AspNetCore . Server . Kestrel ": "1.0 . 0 ", 11 Microsoft .Extensions.Logging .C onsole 11 : " 1.0.0 11 , "Microsoft . AspNetCore.Mvc 11 : 11 1.0.0 11
Глава 5. Работа с Razor 117 После сохранения изменений , внесенных в proj ect . j son, среда Visual Studio добавит в проект сборку Microsoft. AspNetCo re. Mv c . Далее мы включаем инфра­ структуру MVC с ее стандартной конфигурацией в файле Startup . cs (листинг 5.2). Листинг 5.2 . Включение инфраструктуры MVC в файле Startup. cs using Microsoft.AspNetCore . Builder ; using Microsoft . AspNetCore.H osting ; using Microsoft.AspNetCore .H ttp; using Microsoft . Extensions .D ependencyinj ection ; using Microsoft . Extens i ons .Logging ; namespace Razor { puЬlic class Startup puЬlic void ConfigureServices( I ServiceCollection servi ces) { services.Adc!Мvc(); puЫic void Configure(IApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { app.UseMvcWithDefaultRoute(); Определение модели Затем мы созда ем папку Models и добавляем в нее файл класса по имени Product . cs с приведенным в листинге 5.3 определением простого класса модели. Листинг 5.3 . Содержимое файла Product. cs из папки Models namespace Razor . Models { puЫic class Product { puЫic int ProductID { get; set ; puЫic string Name { get; set ; } puЬlic string Description { get ; set; puЬlic decimal Price { get ; set; puЫic string Category { set ; get ; } Создание контроллера Стандартная конфигурация, установленная в файле Startup . cs , следует соглаше­ нию MVC относительно отправки запросов контроллеру по имени Ноте по умолчанию. Мы создали папку Controllers и добавили в нее файл класса HomeController. cs , поместив в него простое определение контроллера (листинг 5.4).
118 Часть 1. Введение в инфрастру к туру ASP. NET Core MVC Листинг 5.4 . Содержимое файла HomeController. cs из папки Controllers using Microsoft . AspNetCore.Mvc ; using Razor . Models ; namespace Razor.Contro l lers { puЬl i c class HomeController : Controller p uЫ ic ViewRes ult Index() { Product myP rod u ct = new Product { ProductID = 1, Name = "Kayak", }; Description = " А boat f or one person ", Category = " Watersports ", Price = 275 М r etu rn View(myP roduct); В контроллере Ноте определен метод действия по имени Index () , в котором со ­ здается объект Product с заполнением его свойств. Объект Product передается ме­ тоду View () ,так что он используется как модель во время визуализации представле ­ ния. При вызове метода View () имя файла представления не указывается , поэтому будет применяться стандартное представление для метода действия. Создание представления Чтобы создать стандартное представление для метода действия Index () , мы со ­ здаем папку Views/ Hom e и добавляем в нее файл типа MVC View Page (Стр аница пр ед ­ ставления MVC) по имени Index . cs html, куда помещаем содержимое, показанное в листинге 5.5 . Листинг 5.5 . Содержимое файла Index. cshtml из папки Views/Home @model Razor . Models . Product @{ Layout = nul l; < ! DOCTYPE html > <html > <head> <meta name= " viewpo rt" content= " wid th=device - width " /> <tit le > Index </ title> </hea d > <body> Conte nt will go here </body> </html >
Глава 5. Работа с Razor 119 В последующих разделах мы рассмотрим различные части представления Razor и продемонстрируем разнообразные вещи. которые с ним можно делать. При изучении Razor полезно помнить. что представления существуют для выражения пользователю одной или более частей модели - и это означает генерацию НТМL-разметки. которая отображает данные, извлеченные из одного или множества объектов. Если не забы­ вать, что мы всегда пытаемся строить НТМL-страницу, которая может быть отправ­ лена клиенту. тогда вся активность механизма визуализации Razor обретает смысл. Запустив приложение. вы увидите простой вывод, приведенный на рис. 5.1. [J lndex 1--~ С [ф localhost:60753 L o11tent \vill go 11е1·е --- --- Рис. 5.1 . Выполнение примера прило жения Работа с объектом модели Давайте начнем с самой первой строки в файле представления Index. cshtml: @model Razor.Models .Product Выражения Razor начинаются с символа@. В данном случае выражение @model объявляет тип объекта модели, который будет передаваться представлению из метода действия. Это позволяет ссылаться на методы, поля и свойства объекта модели пред­ ставления посредством @Model, как показано в листинге 5 .6 , в котором приведено простое дополнение к представлению Index. Листинг 5.6 . Ссылка на свойство объекта модели представления в файле Index. cshtml @mode l Razor.Models.Product @{ Layout = null; < !DO CTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> @Model.Name </body> </html>
120 Часть 1. Введение в инфраструктуру ASP.NET Core MVC На заметку! Обратите внимание, что тип объекта модели представления объявляется с ис­ пользованием @model (со строчной буквой m), а доступ к свойству Nam e производится с применением @Model (с прописной буквой М). Поначалу зто может немного запутывать, но со временем станет вполне привычным . Запустив приложение, вы увидите вывод, представленный на рис. 5.2 . [j lndox х "·· С [QS-;-~~ 1110~1:w7sз --~--------==*l ~-------·· ' . . ·-·- - - Kaya k ---· ·------.·~------·--·-·--··~-·---·-w-·--w-·--·---*-"•-...,•-• Рис. 5.2 . Результат чтения значения свойства внутри представления Представление, которое использует выражение @model для указания типа, назы­ вается строго типизированным представлением. Среда Visual Studio способна при­ менять выражение @mo d e l для открытия окна со списком предполагаемых имен чле­ нов, когда вы вводите @M od e l с последующей точкой (рис. 5.3). Razor - 1:1 Х , ~del R~:tor ./-lodels . Product @{ L~yout ~ null; <!ООСТУРЕ html > ~ B <html> . B<t,ead> ·i <me ta nэrr.e•"viewport" contenta'·\ -..i dth =device·\"lidth'' /> i <title>l ndex</title> •l </head> B <body > i l!t'lodel .J11 1</Ьоdу> ., , , Category [ </html> - " Oe<cription Ф Equ ols ·Ф Gt:tHashCode i 100% -~ tl __ ,.._ __ _ _ _ Ф GetType "' ---~­ ,,, Pric e - " ProductlD ,Ф . ToS.tring l+- Рис. 5.3. Среда Visual Studio предлагает список предполагаемых имен членов на основе выражения @Model Список предполагаемых имен членов, отображаемый Visual Studio, помогает из­ бегать ошибок в представлениях Razor. При желании вы можете игнорировать эти предположения, и тогда Visual Studio будет подсвечивать проблемные имена членов,
Глава 5. Работа с Razor 121 чтобы вы внесли исправления, как поступает в обычных файлах классов С#. Пример подсветки проблемы можно видеть на рис. 5.4, где мы пытается сослаться на свойство @Mo de l . No tAR e alProperty. Среда Visual Studio обнаруживает, что класс Pro duc t, указанный в качестве типа модели, не содержит такого свойства, и подсвечивает ошибку в редакторе . .1· ,r. FJ<head) l l <rneta n an;e ="vie1~port " conte n t " "1~idtl1=device-wi d t h " /> <title>I nd ex</title> </head> Э <Ьоdу> l @f'todel.~e~y,, </body> · </html> ~ ~...; ,..," ".~ , .... .м. ~ r Рис. 5.4. Среда Visual Studio сообщает о проблеме с выражением @M o de l Использование файла импортирования представлений При определении объекта модели в начале файла Index . cshtml необходимо было включать пространство имен, которое содержит класс модели, например: @mod el Razor.Models.Pro duct По умолчанию все типы, на которые производится ссьmка в строго типизирован­ ном представлении Razor, должны уточняться своими пространствами имен. Это не особо крупная проблема, когда есть только ссылка на объект модели, но понимание представления может значительно затрудниться при написании более сложных вы­ раж ений Razor, таких как рассматриваемые позже в настоящей главе. Добавив в проект файл импортuрования представлений. можно указать набор про­ странств имен, в которых должен осуществляться поиск типов. Файл импортирования представлений размещается в папке Vi e ws и имеет имя _View impo rt s . c sht ml. На заметку! Файлы в папке Vi ew s, имена которых начинаются с символа подчеркивания ( ), пользователю не возвращаются , что позволяет с помощью имен файлов проводить различие между представлениями, подлежащими визуализации, и поддерживающими их файлами. Файлы импортирования представлений и компоновки (будут описаны вскоре) имеют имена, начинающиеся с символа подчеркивания. Чтобы создать файл импортирования представлений, щелкните правой кнопкой мыши на папке View s в окне Solution Explorer, выберите в контекстном меню пункт Addc::>New ltem (Добавить<=:> Новый элемент) и укажите шаблон MVC View lmports Page (Страница импортирования представлений МVС) из категории ASP.NET (рис. 5.5). Среда Visual Studio автоматически назначит файлу имя_Vi ew i mp o r t s. cshtml, а щелчок на кнопке Add (Добавить) приведет к его созданию. Поместите в файл выра­ жение, показанное в листинге 5.7 .
122 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Clitnt·side Code t> Onl ine Nыn<: MVC View l •yout Page MVC у;..,, St4rt Page ASP. NП Ccnfig ur ation File ~е to 90 onlin e and find templates. _ Vie wlmports.c shtn' I ASP.NEТ ASP.NEТ ASP .NEТ • Тур~: ASP.N~ MVC Vi e\v lmpc t J'! ·. Рис. 5.5 . Создание файла импортирования представлений Листинг 5.7. Содержимое файла_Viewimports. cshtml из папки Views @using Razor . Mod e ls Пространств а имен , в которых должен проводиться поиск классов, пр именя емых в представлениях Razor, указываются с использованием выражения @using, за кото ­ рым следует пространство имен. В листинге 5. 7 добавлена запись для пространства имен Razor . Models , содержащего класс модели в при м ере приложения . Теперь, когда пространство имен Ra z or . Models включено в файл импортирова ­ ния представлений, можно удалить пространство имен из файла Index . cshtml (лис­ тинг 5.8). Листинг 5.8 . Ссылка на класс модели без пространства имен в файле Index. cshtml @model Product @{ Layout = null ; < ! DOCTYPE html> <html> <head> <meta name= "vi ewport " content="wi dth=devi ce-width" /> <t i tle> I ndex</title> </ h ead> <body> @Model.Name </body> </html >
Гл ава 5. Работа с Razor 123 Совет. Выражение @using м ожн о также до бавлять в отдельные файлы представл ений, ч то позволит применять внутри них типы без указания пространства имен. Работа с компоновками Ниже приведено еще одно важное выражение Razor из файла представления Index. cshtml: @{ Layout null; Это пример блока кода Razor, который позволяет включать в представление опе­ раторы С#. Блок кода открывается посредством @{ и закрывается с помощью }, а содержащиеся в нем операторы оцениваются при визуализации представления . Показанный выше блок кода устанавливает значение свойства Layou t в null. Представления Razor компилируются в классы С# внутри приложения MVC, а в ба­ зовом классе, который они используют, определено свойство Layout. В главе 21 объ­ ясняется, нак все это работает, а пока достаточно знать, что результатом установки свойства La yout в null является сообщение инфраструктуре МVС о том, что наше представление является самодостаточным, и оно будет визуализировать все свое со­ держимое, которое необходимо возвратить клиенту. Самодостаточные представления хороши для простых примеров приложений, но реальный проект может включать десятки представлений. и некоторые представле­ ния будут иметь общее содержимое. Дублированное общее содержимое в представ­ лениях становится трудным в управлении, особенно если нужно внести изменение и отследить все представления, подлежащие модификации. Более эффективный подход предусматривает использование компоновки Razor, яв­ ляющейся шаблоном, который хранит общее содержимое и может быть применен к одному или большему числу представлений. Когда вы вносите изменение в компонов­ ку , оно автоматически воздействует на все представления, которые ее используют. Создание компоновки Компоновки обычно совместно используются представлениями , применяемыми множеством контроллеров, и хранятся в папке по имени Views/Shared, которая входит в перечень местоположений, просматриваемых Razor в попытках найти файл. Чтобы создать компоновку. создайте папку Views/Shared, щешшите на ней правой кнопкой мыши и выберите в контекстном меню пункт AddФNew ltem (ДобавитьФНовый элемент). Укажите шаблон MVC View Layout Page (Страница компоновки представле­ ний МVС) в категории ASP.NET и введите _BasicLayo ut . cshtml в качестве имени файла (рис. 5.6). Щелкните на кнопке Add (Добавить) для создания файла. (Подобно файлам импортирования представлений имена файлов компоновок начинаются с символа подчеркивания .) В листинге 5.9 показано начальное содержимое файла _Basi c Layout . cshtml, до­ бавленное средой Visual Studio при создании файла.
124 Часть! . Введение в инфраструктуру ASP.N ET Саге MVC " lnstall<d f'Se;rc~' ln<talled Templ•lf ASP .Nff ~· Туро: ASP.NEТ fl; MVC Controller Closs ASP . NEТ Clie nt-s id• MVC Vi.,,v Loyout Pog Code ~·fl; Web AP I Cont ro ll er Cla1S ASP.N EТ fl- Online . ~· МVС View lmp or1s Pog e ASP .NET ~·fl; Ritzor Tag H~lper ASP.NET "' Cljclc here to go onlioe itnd find tempi<!t es. _Bo sicl"yout.c sl,tml Рис. 5.6. Создание компоновки Листинг 5.9. Начальное содержимое файла _BasicLayout. cshtml <!DOCTYPE html> <html> <head> <m eta name="viewport " content="width=device-width" /> < title> @ViewBag.Title</title> </head> <body> <div> @RenderBody () </div> </body> </html> i Компоновки - это специализированная форма представлений; в листинге 5.9 выражения @ выделены полужирным . Вызов метода @RenderBody () вставляет в разметку компоновки содержимое представления. указанное с помощью метода действия. Другое выражение Razor в компоновке обращается к свойству по имени ViewBag . Ti tle для установки содержимого элемента ti t le . Объ ект ViewBag явля­ ется удобным средством, которое позволяет передавать значения данных внутри при­ ложения - в рассматриваемом случае между представлением и его компоновкой . Вы увидите, как это работает, когда компоновка будет применена к представлению. Элементы HTML в компоновке будут применены к любому представлению. которое использует компоновку , предоставляя шаблон для определения общего содержимого. В листинге 5.1 О к компоновке добавлена простая разметка, чтобы ее эффект как шаб­ лона был очевидным.
Глава 5. Работа с Razor 125 Листинг 5.10. Добавление содержимого в файл_BasicLayout. cshtml < !DOCTYPE html> <html> <head> <meta name= "viewport " content= " width=device - width" /> <title>@ViewBag .Ti tle</title> <style> #mainDiv { padding: 20рх; border: solid medium Ыасk; font-size: 20 pt } </style> </head> <body> <hl>Product Information</hl> <div id="mainDiv"> @RenderBody () </div> </body> </html > Здесь был добавлен элемент заголовка, а также стиль CSS для стилизации со­ держимого элемента div, в котором находится выражение @RenderBody (), прос­ то ради прояснения, какое содержимое поступает из компоновки, а KaJ{Oe - из представления. Применение компоновки Чтобы применить компоновку к представлению, понадобится установить значение свойства Layou t и удалить НТМL-разметку, которая теперь будет предоставляться компоновкой, такую как элементы html, head и body (листинг 5.11). Листинг 5.11. Применение компоновки в файле Index. cshtml @model Product @{ Layout = "_BasicLayout"; ViewBag .Title = "Product Name"; Product Name: @Model.Name Свойство Layout указывает имя файла компоновки, который будет использовать­ ся для представления, без расширения cshtml . Механизм Razor будет искать указан­ ный файл компоновки в папках /Views/Home и Views/Shared . Кроме того, выполнено присваивание свойства ViewBag. Ti tle . Оно будет приме­ няться компоновкой для установки содержимого элемента t i t 1 е при визуализации представления. Трансформация представления значительна даже для такого простого приложе­ ния. Компоновка располагает всей структурой, требуемой для любого НТМL-ответа , оставляя представлению только заботу о динамическом содержимом, которое отоб-
126 Часть 1. Введение в инфраструктуру ASP.NET Core MVC ражает данные пользователю. Когда инфраструктура MVC обрабатывает файл Index. cshtml, она применяет компоновку для создания унифицированного НТМL­ ответа (рис. 5 .7). х , -> С ' CJ localhost:6063 '1 :=J 1 . --------- --- --- P1·oduct Information Ргоdнсt Nаше: Kayak Рис. 5. 7. Результат применения компоновки к представлению Использование файла запуска представления Осталась еще одна небольшая шероховатость, которую необходимо устранить - мы должны указывать файл компоновки для применения в каждом представлении. Следовательно, если нужно переименовать файл компоновки, то придется отыскать все ссылающиеся на него представления и внести изменение, что будет чреватым ошибками процессом и противоречит лейтмотиву, пронизывающему разработку при­ ложений MVC - легкости сопровождения. Решить упомянутую проблему можно с использованием файла запуска представ­ ления. При визуализации представления инфраструктура МVС ищет файл по имени _ V iewStart . cshtml. Содержимое этого файла будет трактоваться так, если бы оно присутствовало внутри самого файла представления , и данное средство можно при­ менять для автоматической установки свойства Layout . Чтобы создать файл запуска пр едставления, щелкните правой кнопкой мыши на папке Views, выберите в контекстном меню пункт AddqNew ltem (ДобавитьqНовый элемент) и укажите шаблон MVC View Start Page (Файл запуска представления МVС) в категории ASP.NET (рис . 5.8). Среда Vlsual Studio автоматически назначит файлу имя _ViewStart.cshtml; щелчок на кнопке Add (Добавить) приведет к созданию файла с начальным содержи­ мым, приведенным в листинге 5.12. Листинг 5.12. Начальное содержимое файла ViewStart. cshtml @{ Layout "_Layout "; Для применения компоновки ко всем представлениям в приложении значение, присваиваемое свойству Layout, изменено, как показано в листинге 5.13.
Глава 5. Р абота с Aazor 127 . Туре: ASP.N ET MVC View l"yout Page ASP.NEТ Client~side MVC View St•rt Page Code ~ Online ji" MVC View lmports Page ASP.NEO # ~·t.; Razo r Т119 Helper ASP.NET ~· 5 ~ Middleware Class ASP.NEТ ~·t.; Startup class ASP.NEТ , у1] ASP.NEO Configurotion File ASP.NEТ Cli<:k lч~re !О go onli ne § ПО [ind A~ml2; 1 c1t~ . Nome: _V iewStart .cshtml Рис. 5.8 . Создание фай л а запуска представления Листинг 5.1 З. Применение стандартного представления в файле_ViewStart. cshtml @{ Layout = "_BasicLayout"; Поскольку файл запуска представления содержит значение для свойства Layout, можно удалить соответствующее выражение из файла Iпd e x . c shtml (листинг 5.14). Листинг 5. 14 . Обновление файла Index. cshtml, позволяющее отразить использование файла запуска представления @model Product @{ ViewBag . Ti t le = " Pr oduct Name"; Produc t Na me: @Model.Name Специально указывать, что должен применяться файл запуска представления, не требуется . Инфраструктура MVC автоматически обнаруживает данный файл и ис­ пользует его содержимо е . Преимущество отдается значениям , определяемым в фай­ ле представления, что делает переопределение файла запуска представления очень простым. Можно также применять несколько файлов запуска представлений , чтобы устано­ вить стандартное поведение для разных частей приложения . Механизм Razor ищет самый близкий файл запуска для обрабатываемого представления, а это значит, что стандартные настройки можно переопределять, добавляя файл запуска представле­ ния в папку View s /Home или Views / Share d.
128 Часть 1. Введение в инфрастру к туру ASP.NET Core MVC Внимание! Важно понимать разницу между отсутствием свойства Layout в файле пред­ ставления и его установкой в null. Если представление является самодостаточным, и вы не хотите применять компоновку, то установите свойство Layout в n ul l. Если же просто опустить свойство Layo u t , то инфраструктура MVC будет считать, что компоновка вам необходима, и она должна использовать значение, которое найдет в файле запуска представления. Использование выражений Razor Теперь , когда вы видели основы представлений и компоновок, давайте переклю­ чимся на другие виды выр ажений, поддерживаемые Razor, и посмотрим , к а к их при­ менять для создания содержимого представлений. В хорошем приложении MVC им е ­ ется четкое разделение между ролями метода действия и представления . Роли просты и кратко описаны в табл. 5.3. Таблица 5.3. Роли, исполняемые методом действия и представлением Компонент Метод действия Представление Что делает Передает представлению объект модели представления Использует объект модели пред­ ставления для отображения со­ держимого пользователю Чего не делает Не передает представлению сформатированные данные Не изменяет ни одного аспе кта в объекте модели представления Далее в книге мы будем неоднократно возвращаться к этой теме. Чтобы извлечь максимум из MVC , необходимо соблюдать и обеспечивать разделение между разными частями приложения. Вы увидите , что механизм Razor позволяет делать очень мно­ гое , включая применение операторов С# , но вы не должны использов ать Razor для выполнения бизнес-логики или манипулирования объектами моделей предметной области. В качестве простого примера в листинге 5.15 демонстрируется добавлени е нового выраж ения в представление I n d ex . Листинг 5.15. Добавление выражения в файл Index. cshtml @mode l Prod u ct @{ ViewBag . Title = " Product Name "; <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> Значени е свойства Price можно было бы сформатировать в м етоде действия и п е ­ редать его представлению . Это бы работало, но такой подход разрушает пр еимущест­ во шаблона МVС и сокращает возможность реагировать на изменения в будущем. Как уже упоминалось, мы возвратимся к данной теме снова , но вы должны запо мнить , что инфраструктура ASP.NEТ Core MVC не принуждает правил ьно применять паттерн МVС и нужно учитывать влияние решений, принимаемых во время проектирования и написания кода .
Глава 5. Работа с Razor 129 Обработка или форматирование данных Важн о отличать обработку данных от их форматирования. Представления форматируют дан­ ные , поэтому в предыдущем разделе представлению передается объект Product, а не отображаемая стро ка с форматированными свойствами этого объекта. За обработку данных, включая выбор объе ктов данны х для отображения, отвечает контроллер , который будет об­ ращаться к модели с целью получения и модификации требующихся данных. Иногда трудно понять, где проходит черта между обработкой и форматированием, но в качестве эмпири ­ ческого правила рекомендуется занять осторожную позицию и выносить из представления в контроллер все кроме просте й ших выраже ний. Вставка значений данных Самое простое, что можно делать с помощью выражения Razor - вставлять значе­ ния данных в разметку. Наиболее распространенный способ сделать это предполага­ ет использование выражения @Model . Представлени е Index уже содержит примеры применения такого подхода, подобные показанному ниже: <p>Prod uct Name : @Model.Name </p> Вставлять значения можно также с использованием объекта ViewBag, кото­ рый применялся для установки содержимого элемента t i tle в компоновке . Объ ект ViewBag мож но использовать для передачи данных из контроллера в представление, дополняющее модель (листинг 5 .1 6) . Листинг 5.16. Применение ViewBag в файле HomeController. cs using Microsoft . AspNetCore .Mvc; using Razor.Models; namespace Razor . Controlle rs { puЬlic class HomeController : Controller puЬlic ViewResult Index() { Product myProduct = new Product { ProductID = 1 , }; Name = " Kayak", Description = " А boat for one person ", Category = "W atersports ", Price = 275 М ViewBag. StockLevel = 2; return View(myProduct); Свойство ViewBag возвращает динамический объект, который может использо­ ваться для определения произвольных свойств. В листинге 5.16 было определено и установлено в 2 свойство по имени StockLevel. Так как объект ViewBag является динамическим, объявлять имена свойств заранее необязательно, но это означает, что
130 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC для свойств ViewBag среда Visual Studio не в состоянии предоставлять предполагае­ мые варианты автозавершения. Знание того, когда применять ViewBag, а когда расширять модель - вопрос опы­ та и сложившихся предпочтений. Мой персональный стиль заключается в том, что­ бы использовать объект ViewBag только для предоставления визуальных подсказок о способе визуализации данных и не применять его для значений данных, которые отображаются пользователю. Но зто просто то, что подходит лично мне. Если вы хо­ тите использовать ViewBag для данных, отображаемых пользователю, тогда обращай­ тесь к значениям с помощью выражения @ViewBag (листинг 5.17). Листинг 5.17 . Отображение значения ViewBag в файле Index. cshtml @model Product @{ ViewBag.Title = " Product Name "; <p>Product Name : @Model.Name</p> <p>Product Price: @($ " (Model.Price:C2} " )</p > <p>Stock Level: @ViewBag.StockLevel</p> Результат можно видеть на рис. 5.9 . ~ Product N"m~ Х 1 С ~- localho ;GO~ - ---- -------· - - _ j] ~-1 1 P1·oduct I11fo1·matio11 1 1 Product Name: Kayak 1 1 ::::::::~~е2$275 00 1 i 1 L_ ... _ _ -_ -_ -----_ -_ -_- _ - _ =:_-:_ -_ -_ - _ - _ - _ - _ - __ -_ -_ - _ -_ - _ ::_ -- .._ -: -:-:-: -:-:. .._, Рис. 5.9. Применение выражений Razor для вставки значений данных Установка значений атрибутов Во всех рассмотренных до сих пор примерах устанавливалось содержимое элемен­ тов, но выражения Razor можно использовать также для установки значений атри­ бутов элемента . В листинге 5.18 демонстрируется применение выражений @Model и @ViewBag для установки содержимого атрибутов в элементах представления Index.
Глава 5. Работа с Razor 131 Листинг 5.18. Использование выражений Razor для установки значений атрибутов в файле Index. cshtml @model Product @{ ViewBag . Title = " Product Na me "; <div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel"> <p>Product Name : @Model . Name</p> <p>Product Pr i ce : @($ "{ Model . Price : C2 }" )</p> <p>Stock Level : @ViewBag . StockLevel</p> </div> Выраж е ния Razor пр и меняются для установки знач е ний неко т орых атрибутов данных в элементе div . Со вет. Атрибуты данных , которые представляют собой атрибуты с именами, начинающимися на data- , в течение многих лет были неформальным способом создания специальных атрибутов и затем сделаны частью формального стандарта HTML5. Чаще всего они ис­ пользуются так , что код JavaScript может находить специфические элементы, или так , что стили CSS могут применяться более ограниченным образом. Запустив прим ер приложения и прос мотрев НТМL-разм етку , отправленную брау­ зе ру, вы увидите, что механизм Razor установил значения атрибутов : <div data-stocklevel="2" data-productid="l"> <p>Product Name : Kayak</p> <p>Product Price : $2 7 5 . 00</р> <p>Stock Level : 120</р> </div> Использование условных операторов Механизм Razor спо собен обр абатывать условны е операторы, что озн ачает воз­ можность настройки вывода из представления на основ е значений данных представ ­ ления . Такой при ем является наиболее важной частью Razor и позволя ет создавать сложн ы е и и зм е нчивые компоновки , которые по-прежн е му остаются простыми в пони мании и сопровождении. В листинг е 5 . 19 прив едено обновленное содержи мое представл ения Index, включающ ее условный оператор . Листинг 5.19. Применение условного оператора Razor в файле Index. cshtml @model Product @{ ViewBag . Title = " Produc t Name "; <div data- producti d= " @Model . Pr odu ctID" data-stocklevel= " @Vie wBag . StockLeve l" > <p>Product Name: @Model. Na me</p> <p>Product Price : @($ " {Model . Price : C2 }" )</p> <p>Stock Level :
132 Часть 1. Введение в инфраструктуру ASP.NET Core MVC @s witch ( (int)Vie wBag . StockLe vel ) caseО: @:Out of Stock brea k; case 1: case 2: case 3: <b>Low Stock (@V iewBag . StockLeve l)</b> break; defaul t: </р> </ div> @: @ViewBa g. StockLev e l in Sto ck break; Чтобы начать условный оператор , перед условным ключевым словом С# (в этом примере switch) помещается символ @ . Блок кода завершается закрывающей фигур­ ной скобкой (}) подобно обычному блоку ~юда С#. Совет. Обратите внимание, что для использования внутри оператора sw i tch значение свойства Vi ewBag . ProductCount должно быть приведено к типу int. Причина в том, что Rаzоr-выра жение @sw i tch не может оценивать динамическое свойство - необходи ­ мо приведение к специфичному типу, чтобы было известно , как выполнять сравнения. Внутри блока кода Razor в вывод представления можно включать НТМL- элементы и значения данных , просто определяя НТМL-разметку и выражения Razor, например : <Ь>Low Stock (@ViewBag.StockLevel)</b> Помещать элементы или выражения в кавычки либо отмечать их каким-то спе­ циальным образом не требуется - механизм Razor будет интерпретировать это как вывод, подлежащий обработке. Тем не менее, если нужно вставить в представление литеральный текст, когда он не содержится в НТМL-элементе, то об этом понадобится сообщить Razor, добавив к строке префикс: @: Out of Stock Символы @: пр едотвращают обработку механизмом Razor данной строки как опе­ ратора С#, что является стандартным поведением в отношении те кста . Результат вы­ полнения такого условного оператора показан на рис. 5.10. Условные операторы играют важную роль в представлениях Razor, т.к. они позво ­ ляют варьиров ать сод е ржимое на основе значений данных, которые пр едставл ение получает от метода действия. В качестве дополнительной демо нстрации в листин­ ге 5.20 приведено представл ение Index . c s h t ml с добавленным оператором if - еще одним распространенным условным оператором.
Глава 5. Работа с Razor 133 С) ProdU<t N""' Х 1- С (~ l~~~~~t~60i_S3 -==--=·=-=--·=--==='---~ 1 P1·oduct Info1·mation 1 1 Prodнct Nаше: Kayak 1 P1·od.нct PI"ice: $275.00 1 Stock Level: Lo\V Stock (2) l_ ---- --- ----------------------------- Рис. 5. 1о. Применение оператора sw i tch в представлении Razor Листинг 5.20. Использование оператора if в файле Index. cshtml @model Product @{ ViewBag.Title = " Product Name "; <div data - productid= " @Model . ProductID " data - stocklevel= " @ViewBag . StockLeve l" > <p>Product Name : @Model . Name</p> <p>Product Price : @($ "{ Model.Price : C2} ") </p> <p>Stock Level : @if (ViewBag.StockLevel == 0) @: Out of Stock } else if (ViewBag. StockLevel > О && ViewBag. StockLevel <= 3) { <b>Low Stock (@ViewBag.StockLevel)</b> else { @: @ViewBag.StockLevel in Stock } </р> </div> Этот условный оператор выдает те же самые результаты, что и оператор swi tch , но мы просто хотели проде монст рировать применение условных операторов С# в представлениях Razor. В главе 21, где представления рассматриваются более подроб­ но, будут даны объясн ения, как все это работает. Проход по содержимому массивов и коллекций При разработке приложения МVС часто необходимо выполнять проход по содер­ жимому массива или друrой разновидности коллекции объектов с генерацией подроб­ ной информации для каждого объекта. Чтобы продемонстрировать, как это делается,
134 Часть 1. Введение в инфраструктуру ASP.NET Core MVC в листинге 5.21 показан модифицированный метод действия I ndex () в контроллере Home , 1<0торый теперь передает представлению массив объектов Pr odu ct . Листинг 5.21. Использование массива в файле HomeController. cs using Mi crosoft . As pNetCo re. Mvc ; usi ng Razor.Models; namespace Razor . Controllers { puЫic class HomeController : Controller puЫi c IActionResult Index () { Product [] array = { new Product {Name = new Product {Name new Product {Name new Product {Name }; return View(array); "Kayak", Price = 275 М}, "Lifejacket", Price = 48. 95 М}, "Soccerball", Price 19.50М}, "Corner flag", Price = 34.95 М} Метод действия Index ( ) создает объект Produ ct [] , который содержит простые значения данных, и передает его методу Vi ew () для визуализации с применени ем стандартного представления. В листинге 5.22 изменен тип модели для пр едставления Index и с помощью цикла fore ach производится проход по объектам в массиве. Листинг 5.22 . Проход по массиву в файле Index. cshtml @mode l Product[J @{ ViewBag .T itl e <taЬle> "Product Name "; <thead> <tr><th>Name</th><th>Price</th></tr> </thead> <tЬody> @foreach (Product р in Model) <tr> <td>@p.Name</td> <td>@($"{p.Price:C2}")</td> </tr> } </tЬody> </taЬle> Оп ератор @f orea c h выполняет проход по содержимому массива объектов моде­ ли и генерирует для каждого элемента массива строку в таблице. Вы видите, что в цикле fo r each создана локальная переменная по имени р , на сво й ства которой осу­ ществляется ссылка с использованием выражений Razor вида @р . Name и @р . Price. Результат приведен на рис. 5.11 .
Резюме ['j Product Nome Х . С Гф lo-ca-111-os -t .60- 75- =3==== = -- --- ~-==---= - - --=:-:- _ -:::::-::;::_-:::;:::::::-_-: -::::::::-: ..- ._ - ________ ---- Product Info1·mation Name Price Kayak $275.00 Lite j ac ket $48.95 Soccer ball $ 19.50 Согпе1· flag $34.95 ---------------- Глава 5. Работа с Razor 135 Рис. 5.11 . Применение Razor для прохода по массиву В этой главе был предложен обзор механизма визуализации Razor и показано, как его использовать для генерации НТМL-разметки. Мы взглянули, каким образом ссы­ латься на данные, передаваемые из контроллера, через объект модели представления и объект Vi e wBa g, а также продемонстрировали применение выражений Razor для настройки ответов пользователю на основе значений данных. В оставшихся главах книги вы увидите много разных примеров использования Razor, а в главе 21 найдете подробное обсуждение функционирования механизма визуализации МVС. В следую­ щей главе будут описаны некоторые средства, предлагаемые Visual Studio для работы с про ектами ASP.NET Core MVC .
ГЛАВА 6 Работа с Visual Studio в этой главе рассматриваются основные средства, предоставляемые Visual Studio для разработки проектов ASP.NET Core MVC . В табл. 6.1 приведена сводка по главе. Таблица 6. 1. Сводка по главе Задача Добавление к проекту пакетов .N ET Добавление к проекту пакетов JavaScript или CSS Просмотр результатов изменения представления или класса Отображение детальны х сообще­ ний в браузере Получение детальной информа­ ции и контроля над выполнением приложения Повторная загрузка одного или большего числа браузеров с при­ менением Visual Studio Сокращение количества НТТР­ запросов и ширины полосы про­ пускания, требуемой для файлов JavaScript и CSS Решение Отредактируйте раздел зависимостей (dependencies) файла proj ect . j son или воспользуйтесь инструмен том NuGet Создайте файл bower. j son и добавь­ те требующиеся пакеты в его раздел dependencies Используйте модель итеративной разработки Используйте страницы исключений разработчика Используйте отладчик Используйте средство Browser Link (Ссылка на браузер) Листинг 6.1 -6 .6 6.7, 6.8 6.9-6.11 6.12 6.13 6.14 -6 .16 Используйте расширение Bundler & Minifier 6 .17-6 .28 (Упаковщик и минификатор) На заметку! Ка к объяснялось в главе 2, в Microsoft заявили , что в будущих выпусках Visual Studio изменят инструменты , которые применяются для создания приложе ний ASP. NEТ Core . Это означает, что приводимые в данной главе инструкции могут устареть . Проверяйте веб-сайт издательства на предмет пересмотренных инструкций, которые я напишу, когда новые и нструменты станут стабильными .
Глава 6. Работа с Visual Studio 137 Подготовка проекта для примера Для ц елей настоящей главы мы создали новый проект ASP.NET Саге Web Application (.NET Саге) (Веб-приложени е ASP.NET Core (.NET Core)) по имени WorkingW ithVisualStudi o, и спол ьзуя шаблон Empty (Пустой). Мы добавили сборку МVС за счет редактирования раздела dependenci es файла proj ect. j son (листинг 6 .1) . Листинг 6.1 . Добавление сборки MVC в файле proj ect. j son " dependenci es " : { " Microsoft . NE TCore . App": " version ": 11 1.0.0", " type ": " platform " }, }' "Microsoft.AspNetCore . Diagnostics": "1.О . О", "Microsoft . AspNetCore . Server .I ISintegrat ion ": " 1 . 0 . 0 ", " Microsoft . AspNetCore . Server . Kestre l": "1. 0 . 0 ", "Microsoft.Ext ensi ons .Logging .Consol e ": "1.0 .0 ", 11 Мicrosoft . AspNetCore. Mvc" : 11 1.О.О 11 Затем мы включили инфраструктуру MVC с ее стандартно й конфигурацией в фай ­ ле Startup . cs (листинг 6.2). Листинг 6.2 . Включение инфраструктуры MVC в файле Startup. cs using Microsoft . AspNetCore . Bui lde r ; using Microsoft .AspNet Core .Hosti ng; using Microsoft . AspNetCo r e . Http ; using Mi crosoft .Extens i ons .Depe n denc yinj ection ; using Microsoft . Extensions . Logg i ng ; namespace Work i ngW i thVi sualStud i o puЫic class Sta r tup { puЫ i c void ConfigureSe r vices (IServi ceCol lecti on se r v i ces) { services.AddМvc(); puЫic void Configu r e (IAp pl i catio nBuilder ар р, IH ost ingEnv i ronment env , ILoggerFact ory l ogg erFact ory) { app.UseMvcWithDefaultRoute(); Создание модели Со здайте папку Mode l s и добавьте в нее файл класса по имени Produ ct . cs с оп­ редел е ни ем, показанным в листинге 6.3 .
138 Часть 1. Введение в инфраструктуру ASP. NЕТ Саге MVC Листинг 6.3 . Содержимое файла Product. cs из папки Models namespace Work i ngWithVisualStudio . Models puЫic class Product { puЫic string Name { get ; set; } puЬlic decimal Price { get ; set; Чтобы создать простое хранилище объектов Product, добавьте в папку Models файл 1<ласса по имени SimpleReposi tory . cs и поместите в н е го опр ед еле ние, при­ веденное в листинге 6.4 . Листинг 6.4 . Содержимое файла SimpleReposi tory. cs из папки Models using System . Collections . Generic ; namespace WorkingWithVisualStudio.Models puЫic class SimpleRepository { private static SimpleRepository sharedRepository = new SimpleRepository( ) ; private Dictionary<string , Product> products = new Dictionary<string , Product>(); puЬlic static SimpleRepository SharedRepository => sharedRepository; puЫic SimpleRepository() { var initialitems = new [] { new Product { Name " Kayak ", Price = 275М }, new Product ( Name "Lifejacket ", Price = 48. 95М } , new Product { Name " Soccer ball ", Price 19. 50М } , new Product { Name "Corner flag", Price = 34.95М } }; foreach (var р in initialitems) { AddProduct(p); puЫic IEnumeraЫe<Product> Products => products.Values; puЫic void AddProduct(Product р) = > products.Add(p.Name, р) ; Класс SimpleReposi tory хранит объекты модели в памяти, т.е. любые внесенные в модель изменения утрачиваются, когда приложение останавлива ется или переза­ пускается . Для примеров этой главы непостоянного хранилища вполне достаточно, но такой подход не может применяться во многих реальных про ектах; в главе 8 рас­ сматривается пример создания хранилища, которое запоминает объекты модели на постоянной основе, используя реляционную базу данных. На заметку! В листинге 6.4 определено статическое свойство по имени SharedRepos i tory, предоставляющее доступ к единственному объекту SimpleReposi tory , который может применяться повсюду в приложении . Это не является установившейся практикой, но я хочу продемонстрировать распространенную проблему, с которой вы столкнетесь при разработке приложений MVC; в главе 18 будет описан более эффективный способ работы с разделяемыми компонентами.
Глава 6. Работа с Visual Studio 139 Создание контроллера и представлени я Добавьте в проект папку Control l er s и поместите в нее ф айл класса по имени HorneController . cs, в котором определен контролл е р. показанный в листинге 6.5 . Листинг 6.5 . Содержимое файла HomeController. cs из папки Controllers u sing Microsoft . Asp NetCore . Mvc ; using WorkingWithVis u alStudi o . Mo d el s; narnespace Worki ngWithVisualStudio.Cont r ol lers puЬlic class HorneContr oll er : Cont r oller { puЫic IAct ionR esult Inde x( ) => View(SirnpleRepository . SharedRepository . Pr oducts) ; Здесь имеется един ственный метод действия Index () ,который получает вс е объ­ екты модели и переда ет их методу Vi ew () для визуализации стандартного представ­ ления. Чтобы добавить это пр едставление, создайте папку Views/Home и поместите в нее файл представления по имени I n dex . cshtml, содержимое которого приведено в листинге 6 .6. Листинг 6.6 . Содержимое файла Index. cshtml из папки Views/Home @model IEnurneraЫe<WorkingWithVisualStudio.Models. Product> @{ Layout = null ; < ! DOCTYPE htrn l> <html> <head> <meta name= " v iewport " c ont ent=" width=device - wi dth " /> <title>Working with Vis ual Studi o</t i t le> </head> <body> <tаЫе> <thead> <tr><td>Narne</td><td >P r ice </td></ t r> </thead> <tbody> @foreach (var р in Model) <tr> <td>@ p.Narne</td> <td>@p . Pr i ce</td> </tr> </tbody> </tаЫе> </body> </html> Представление включает т аблицу, в которой с помощью Rаzоr-цикла f o r each со­ здаются строки для всех объектов модули, причем каждая строка содержит ячейки для значений свойств Name и Pr i c e. Запустив пример приложения, вы увидите ре­ зультаты. п01ш занные на рис. 6.1 .
140 Часть 1. Введение в инфраструктуру ASP.NET Core MVC - -·- ----------- -----·1 С l.~~~~~ost:612~----------···----·--~ -- ------ -------· ----------~- ~- -~ Nаще Pric e Kayak 275 Lifejacket 48.95 Soccer ball 19.50 Сошеr flag 34 .95 Рис. 6.1. Выполнение примера приложения Выбор исполняющей среды .NET При создании нового проекта ASP.NET Core предлагается выбрать один из двух шаблонов проек­ тов с похожими названиями : ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP. NET Core (.NET Core)) и ASP.NET Core Web Application (.NET Framework) (Веб-приложение ASP.NET Core (.NЕТ Framework)). Оба шаблона можно использовать для создания приложений с применением инфраструктуры ASP.NET Core MVC, а отличаются они тем, какая исполняющая сре­ да .NET выполняет код . .NE T Core - это небольшая оптимизированная исполняющая среда, первоначально созданная специально для ASP.NET, но теперь она играет более широкую роль для других типов приложений .N ET. Она была спроектирована как межплатформенная и предоставляет возможности для раз­ вертывания приложений ASP.NET за пределами традиционного набора платформ Windows, а так­ же в легковесные облачные контейнеры, подобные Docker. Исполняющая среда .N ET Core будет поддерживать Windows , macOS, FreeBSD и Linux; она спроектирована как модульная и включает только сборки, которые требуются приложению, что дает в результате меньший и более простой отпечаток для развертывания. Кроме того, АРl-интерфейс .NET Core API также имеет меньшие размеры, поскольку из него изъяты средства, которые являются специфичными для Windows и не могут поддерживаться на других платформах . В краткосрочной перспективе выбор исполняющей среды для проектов будет управляться ис­ пользуемыми инструментами и библиотеками . У независимых поставщиков уйдет некоторое время на обновление своего программного обеспечения для работы с .NET Core и доведение его до уровней стабильности, требуемых для производственного применения. Если вы зависи­ те от пакета либо инструмента, которому необходима полная платформа .NET Framework (или у вас есть унаследованная кодовая база, обновить которую невозможно), тогда при создании проектов ASP.NEТ вам придется использовать вариант ASP.NET Core Web Application (.NET Framework). Вы по-прежнему можете применять все средства, описанные в настоящей книге, и единственное отличие будет связано с исполняющей средой, которая запускает код. Тем не менее, будущим ASP. NEТ является среда .NET Core. Это вовсе не означает, что вы должны немедленно перевести на нее существующие проекты, но означает, что вам не следует создавать новые зависимости от .NET Framework, если их можно избежать, и при выборе новых инструмен­ тов и библиотек понадобится учитывать возможность поддержки ими .NET Core. Дополнительные сведения о .NЕТ Core доступны по адресу https: / /docs .rnicrosoft. corn/en-us/ dotnet/articles/welcorne.
Глава 6. Работа с Visual Studio 141 Управление программными пакетами Проектам ASP.NET Core МVС требуются два разных вида программных пакетов. В последующих разделах будут описаны эти типы пакетов вместе с инструментами, которые среда Visual Studio предлагает для управления ими. Инструмент NuGet Среда Visual Studio предоставляет графический инструмент для управления паке­ тами .NET, который включается в проект. Чтобы открыть этот инструмент, выберите в меню Tools~NuGet Package Manager (Сервис~Диспетчер пакетов NuGet) пункт Manage NuGet Packages for Solution (Управл ение пакетами NuGet для решения) . Инструмент NuGet откроется и отобразит список уже установленных пакетов (рис. 6 .2). Bro'.V$e ~ Updates ConsoHdate St.trth lttft"[) - Mltro,oft.A s pNetCote.Dlllg nostlcs Ьу Mlooio11.o\lpNotC01t.O!•gn1a1ш 8'о ASPJ~fТCo1t middfcw1rt for U(1:pt>0n hcrtdГ.ntJ, e:ctci1tion df1pl.y p•ljt\, 111d di.1gn"'1k~ 1nfo11Ntion. Jrn:lvdes devtloptr U<tp!!On P'!i" m1ddJtw~te, tнq}tiOl'I h•n~. vl.0.0 Mlcrosoft .AspNetCore .Sc:rver.llSJntegratlon bylA1c10'. 0fl .AsJJfle1Co1t.Sm·t _ ..i .o.o t!) ASP.NП Co•t c.cmpotм:nti fc1 wo1l~n9with1~ h~ д!pNt1CcrtM<1dul•. 18 Mlcrosoft.AJpNe1Core.S.rvtr.llSlntegration.Tool s ЬуМЮ> v!Л.O·prlE'М"NHin11 ~ llS1rittg1.1tion yublish t.:io~ for .NfТ Cort ClL Cant11n' tht dctntl·puhll,h· lllt1~•1t 111 comn11rn:1 lc1 pЩ:lnk ing wdi 1ppl1c.1Ucrn !о bt hoottd ustng ltS. • M/crosoft.AspNotCore.Scrvcr.Кo1tre l ЬyM•c101oh.д1ptj(1Ca•c.StNcr.K~t1tl /\SP.Шf Ccrt ~tsщl cro~~·pl1tfc1mwct- ltNtt. vl .Cl.O 1111 Mlcrosoft.Extenslons. Logglng.Con,olc Ьу M1crosott.Ьl:tn11on1.. t o99•n9.Con1 ... 1 .0 .0 -О Con1cl t to99trptov1dtrнт1pltmtnatю11 fo1 Mkro1011 .(;..tcn1iont.L0991n9 111 MlcroJoft. NEТCore.App ЬуМюоw.t ~ дtti cf .NET lJll"~ INI olt' mduded ln the dt!1i.rlt .NП Cort f IK411on modrL ~1.0.0 Manage Packages for Sol u lio n . о 1 ."8 Micro so r.t .AspNetCore.Mvc j Vщlon(,) · 1 5 . 10P1ojtct :_---~-:i ' 0 Ut\Vlor1cincJт'MhVi~u 1.0.О 1 1 1 1 1 -- --- -- __ :J lм t.llled: 1 .!1.О Ot:Кrlpllon ASP.IJE TCottMYC i1.i web fщТitio1cnl: t~t 9r1n you1 p o,.."ttfu~ p1lltt,.,1·b.1~«iw1ytobuild dyntmit .... 11ь1rtei 1r.d wtb AP!~. ASP.NH (Ot( мvс " Рис. 6.2 . Использование диспетчера пакетов NuGet На в1шадке lпstalled (Установленные) приводится сводка по пакетам, которые уже установлены в проекте. Вкладку Browse (Обзор) можно применять для нахождения и установки новых пакетов, а вкладку Updates (Обновления) - для получения списка пакетов, для которых были выпущены свежие версии. Список и местоположение пакетов NuGet Инструмент NuGet управляет содержимым раздела зависимостей (dependencies) файла project . j son , который создается Visual Studio при настройке нового проек­ та, даже когда используется шаблон Empty. На заметку! В Microsoft намерены изменить инструментальную оснастку для ASP.NET в бу­ дущих выпусках Visual Studio. Одно из изменений, которое было анонсировано (но не ре­ ализовано), связано с тем, что файл proj ect. j son больше не будет применяться для управления пакетами NuGet. Проверяйте веб-сайт издательства на предмет обновлений для этой книги, когда компания Microsoft выпустит новые версии .
142 Часть 1. Введение в инфрастру кту ру ASP.NEТ Core MVC Другие разделы файла proj ect. j son будут описаны в главе 14, но если вы вни­ мательно изучите список пакетов, отображаемый инструментом NuGet, то заметите, что он соответствует элементам раздела dependencies, которые для примера проек­ та выглядят следующим образом: 11 dependencies 11 : { 11 Microsoft . NETCore.App ": "version 11 : "1.0.0", "type": "platform" }' }' " Microsoft . AspNetCore . Diagnostics ": "1.0 .0 11 , " Microsoft . AspNetCore.Server . IISintegration" : "1. 0 . О", "M icrosoft.AspNetCore . Server.Kestrel ": " 1.0 . 0 ", " Microsoft . Extensions . Logging . Console": 11 1.0.0 11 , 11 Microsoft . AspNetCore.Mvc 11 : 11 1.0 .0" Каждый пакет указывается с использованием его имени и обязательного номера версии. Некоторые пакеты, такие как Microsoft . NetCore . Арр в примере про екта, имеют дополнительную конфигурационную информацию, как объясняется в главе 14. Среда Visual Studio отслеживает содержимое файла proj ect . j son, т.е. вы можете до­ бавлять или удалять пакеты , редактируя файл напрямую, что и делается повсеместно в книге , т.к. это помогает гарантировать получение ожидаемых результатов , если вы прорабатываете примеры. Когда вы применяете NuGet для добавления в проект какого-то пакета, он автома­ тически устанавливается наряду со всеми пакетами, от которых зависит. Исследовать пакеты NuGet и их зависимости можно, открыв в окне Solution Explorer элемент References (Ссылки), который содержит записи для всех пакетов NuGet в файле proj ect . j son. В результате развертывания записи пакета отображаются пакеты, от которых он зависит (рис . 6.3). Solution Explo rer ФФ_@I~-1. ~-r~!~- 3..:~г:!:~о~:ХУ~!~:.:) _ · - - • ·1 Referenc~ • ili .NEТCoroApp,Vorsi on:v1 .0 t> ·~ Microsoft.AspNetCo re.Oiagnortics (1.0.0) ~ ·~ MicrosoftAspNetCo re.Мvc.ApiExplorer (1.0 .0) ·~ Mic rosoft.AspNetCore.Мvc .Cors (\.О.О) ·~ Microsoft.AspNetCore.Mvc .DataAnnot"tions (1.0 .0) t> ·~ Microsoft .Asp NetCore.Mvc.FormottersJson (1.0 .0} t> ·~ Microsoft.AspNetCore.Mvc.loc"lization (1. 0 .0) ·~ Microsoft.AspNotCore.Мvc .Rdzor (1.0 .0) 0 1lj Microsoft.AspN<Kore.Mvc.TogHelpers (1.0 .0) ·~ Micro s: oft.AspNetCore. Мvc .ViewFeature.s (1.0 .0) ·~ Microsoft.Extensions.Caching.M emory {1.0 .0) ·~ Microsoft. Extensions.Oependencylnjection (1.0 .0) • ·• Microsoft.As pNotCore.Mvc . dl l ~ 0fl! Microsoft.AspNetCo re.Se.rver.llSlnt~ration (1.0 .0) ·~ Microsoft .AspNotCor~Server.Kestrel (1.0.0) 'l lj Microsoft.Extensions.Logging.Console (1.0.0) 0 il} Microsoft.NEТCore.App {1.0.0} ·~ Microsoft,Visua l Studio.\Чeb.Browserlink. loader (14.0 .0) • CJх Рис. 6 . 3. Раздел References окна Solution Explorer
Глава 6. Работа с Visual Studio 143 На рис. 6.3 видно, что при добавлении пакета Mi cros o ft. As pNetCo r e . Mv c в лис­ тинге 6.1 инструмент NuGet загружает и устанавливает его и множество других паке­ тов, которые требуются для разработки приложений MVC . Инструмент Bower Пакет клиентской стороны включает содержимое, которое отправляется клиенту , такое как файлы JavaScript, стили CSS либо изображения . Для управления этими пакетами можно привычно пользоваться инструментом NuGet, но ASP.NEТ Core MVC полагается на новый инструмент под названием Bower. Инструмент с открытым ко­ дом Bower был разработан за пределами Microsoft и мира .NET и широко применялся в разработке веб-приложений, отличных от ASP.NET. В действительности Bower стал настолько успешным , что некоторые популярные пакеты клиентской стороны рас­ пространялись только через Bower. Список пакетов Bower Пакеты Bower указываются в файле bower . j s on . Чтобы создать этот файл , щелк­ ните правой кнопкой мыши на элементе проекта Wo rkingW i thVisua l Studio в окне Sol uti oп Ex plo re r, выберите в контекстном меню пункт Add<=:>New lte m (Добавить <=:> Новый элемент) и укажите шаблон элемента Bowe r C o пfiguratio п Fil e (Файл конфигурации Bower) из категории Clie пt- s i d e (Клиентская сторона), как показано на рис. 6.4 . ASP.NET (lient-side Code ~ Online Sort Ьу: Def•ult TypeScript JSON Configur•tion File Client-s ido n рП1 Co11figuration File Сlien\· •ide rJs Gulp Configurotion File Client -side rJSGrunt Configu r•tion File ClienHide п JSON Schemo Fi le Client-side гJs <iiil>J JS XFile Client-side (lick here to go opline • nd find !empla\б. • Туре: Client-side Bo wer Configuration and .bo 1.;ve rrc . Рис. 6.4. Создание файла конфигурации Bower На заметку! Для загрузки пакетов клиентской стороны Bower использует инструмент g i t. Чтобы обеспечить корректную работу Bower, потребуется заменить версию gi t из Vis ual Studio, как было описано в главе 2.
144 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Среда Visual Studio установит b ow er. j s o n в качестве имени и после щелч­ ка на кнопке Add (Добавить) добавит в проект файл со стандартным содержи мым (листинг 6. 7). Совет. По умолчанию Visual Studio скрывает файл b owe r . j s o n; чтобы его было видно, нуж­ но щелкнуть на кнопке Show All Files (Показать все файлы) в верхней части окна Solution Explorer. Листинг 6. 7. Стандартное содержимое файла bower . j s on "name ": "asp .net", "private": t rue , "depende ncies ": } В листинге 6.8 демонстрируется добавление в файле b ower . j s o n пакета клиент­ ской стороны, что делается включением записи в раздел dependenci es с примене­ нием такого же формата, как в файле proj ect. j son. Совет. Хранилище пакетов Bower находится по адресу h ttp: //bower . io/ sea r ch, где можно искать пакеты для добавления в свои проекты. Листинг 6.8 . Добавление пакетов в файле bower. j son "name ": "asp.net", "private": true, "depende nci e s": { "bootstrap": "3.3.6" Выделенная полужирным строка обеспечивает добавление в пример проекта СSS­ пакета Bootstrap. При редактировании файла b ower. j s on среда Visual Studio предло­ жит список имен пакетов и перечислит доступные версии пакетов (рис. 6 .5). l>(J \'Jork1n9WithVosuolStud•o - bower.1son' - С1Х NuGet • Solu tion • Sch e m a: http ://json.sc hem astore.org/ bower в{ - - - 1 \ "narne" : "asp .net" , "p1·ivate" : tгue , 8 "dependen<ies" : { / "bootstrap" : " ~ f}} ! f..-3.-3.б----./ т~e lot~t ~i~Ы•v•rsion-;r th• ~.~k•g• " ш лз.З.б ~ -3.З.б Рис. 6.5 . Перечисление доступных версий пакетов клиентской стороны
Глава 6. Работа с Visual Studio 145 На момент написания главы последней версией пакета bootst r ap была 3.3 .6. Однако обратите внимание, что Visual Studio предлагает три варианта: 3. 3 . 6, л 3. 3 . 6 и - 3. 3. 6. Номера версий в файле b o we r . j son м огут указываться в виде диапазо­ на разнообразными способами, наиболее полезные из которых описаны в табл. 6.2. Самый безопасный способ указания пакета предусматривает использование явного номера версии. Это гарантирует, что вы всегда будете работать с той же самой верси­ ей до тех пор, пока пр еднамере нно не обновите файл b o wer . j son для запроса другой в е рсии. Таблица 6.2. Ра спространенные форматы для номеров версий в файле Ьower. j son Формат 3.3 .6 * >3.3 .6 >=3 .3 .6 <3.3 .6 <=3 .3 .6 -3.3.6 лз.3.6 Описание Выраже ние номера версии напрямую приведет к установке пакета с точно совпадающим номером версии, например , 3 . 3 . 6 Применение символа звездочки позволяет инструменту Bower загружать и устанавливать любую версию пакета Снабжение номера версии префиксом > или >= позволяет инструменту Bower устанавливать любую версию пакета, которая больше либо больше или равна заданной версии Снабжение номера версии префиксом < или <= позволяет инструменту Bower устанавливать любую версию пакета, которая меньше либо меньше или равна заданной версии Снабжение номера версии префиксом в виде тильды (символ - ) позволяет инструменту Bower устанавливать версии , даже если номер уровня исправ­ лений (последнее число в номере версии) не совпадает. Например, указание -3 . 3. 6 позволяет инструменту Bower устанавливать версию 3 .3.7 или 3.3.8 (которая будет исправлена до версии 3.3 .6), но не версию 3.4.0 (которая бу ­ дет новым младшим выпуском) Снабжение номера версии префиксом в виде символа л позволяет инстру­ м енту Bower устанавливать версии, даже если номер младшего выпуска (вто­ рое число в номере версии) или номер исправления не совпадает. Например, указание л 3 . 3 . О позволит Bower устанавливать версии 3 .3.1 , 3.4.0 и 3 .5.0, но не версию 4 .0.0 Совет. Для примеров в настоящей книге мы создали и отредактировали файл bower . j s o n напрямую . Редактировать файл очень просто , к тому же это способствует получению предсказуемых результатов в случае проработки примеров. Среда Visual Studio также предоставляет графический инструмент для управления пакетами Bower, который можно открыть, щелкнув правой кнопкой мыши на проекте WorkingWithVisualStudio в окне Solution Explorer (родительский элемент файла b o we r. j s o n ) и выбрав в контекстном меню пункт Manage Bower Packages (Управление пакетами Bower) . Среда Visual Studio отслеживает изменения в файле bower . j son и автоматически задействует инструмент Bower для загрузки и уста новки пакетов. Посл е сохранения изменений, внесенных в файл согласно листингу 6.8, среда Visual Studio загрузит па­ кет Bootstrap и установит его в папку wwwroot/ lib (рис. 6.6).
146 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 1> bootstrap t> jquery Рис. 6.6 . Добавление в проект пакетов клиентской стороны Подобно NuGet инструмент Bower управляет зависимостями пакетов, добавляемых в проект. Некоторые расширенные средства пакета Bootstrap завис ят от JavaScript- библиoтeки jQuery, отчего на рис . 6.6 показаны два пакета. Для просмотра списка па­ кетов и их зависимостей необходимо развернуть элемент Depeпdeпcies (Зависимости) в окне Solution Explorer (рис. 6.7). ... ii.J Bo wer ~ ~ bootstrap (3.3 . б) ~ jquery (2.2 .4) "'1:1 х Рис. 6. 7 . Исследование пакетов клиентской стороны и их зависимостей Что произошло с инструментами NPM и Gulp? На начальных стадиях разработки ASP.NET Соге в Microsoft адаптировали два других популярных инструмента с открытым кодом извне экосистемы .NET - NPM и Gulp . Средство NPM представ­ ляет собой диспетчер пакетов для инструментов разработки , который выполняется JаvаSсriрt­ механизмом Node .js , а Gulp является основанным на JavaScript инструментом запуска задач, который позволяет писать сценарии для выполнения задач разработки , таки х как объединение и минификация файлов . Перед самым выпуском ASP.NET Саге 1.0 в Microsoft внесли изменение в ядро и упомянутые инс­ трументы больше не задействуются автоматически в шаблонах проектов MVC. Одна из наиболее распространенны х задач , где использовался инструмент Gulp , теперь выполняется расширением Visual Studio, которое описано в разделе " Подготовка файлов JavaScript и CSS для развертыва­ ния " далее в главе . Среда Visual Studio по-прежнему поддерживает инструменты NPM и Gulp, и они все еще могут применяться для проектов, которые имеют дело со сложным компонентом клиентской стороны . Это может быть полезно, поскольку существуют удобные инструменты и пакеты , которые доступ­ ны толь ко через NPM и могут настраиваться исключительно с использованием Gulp . За деталями обращайтесь в мою книгу Pro Client Development for ASP.NEТ Саге MVC Developers.
Глава 6. Работа с Visual Studio 147 Итеративная разработка Разработка веб-приложений часто может быть итеративным процессом, когда вы вносите небольшие изменения в представления или классы и запускаете приложение, чтобы протестировать результат. Инфраструктура МVС и среда Visual Studio на пару поддерживают такой итеративный подход, делая наблюдение за влиянием изменений быстрым и легким. Внесение изменений в представления Razor Во вр емя разработки изменения, внесенные в представления Razor, вступают в силу, как только поступает Н1ТР-запрос от браузера. Чтобы увидеть это в работе, за­ пустите приложение, выбрав пункт Start Debugging (Запустить отладку) в меню Debug (Отладка) , и после открытия вкладки браузера, отображающей данные, внесите в файл Index. cshtrnl изменения, приведенные в листинге 6.9 . Листинг 6.9 . Внесение изменений в файле Index. cshtml @rnodel IEnurneraЬle<WorkingWithVisualStudio.Models.Product> @{ Layout = null; } < ! DOCTYP E html> <htrnl> <head> <meta name =" v i ewport " content="width=device- width " /> <title>Working with Vi s ual Studio</title> </head> <body> <hЗ>Products</hЗ> <tаЫе> <thead> <tr><td>Name</td><td> Pri ce</ td></ tr> </thead> <tbody> @foreach (var р in Model) <tr> <td>@p .N ame</td> <td>@($"{p.Price:C2}")</td> </tr> } </tbody> </tаЫе> </body> </html> Сохраните изменения в представлении Index и перезагрузите текущую веб-стра­ ницу с помощью кнопки обновления в браузере. Изменения в представлении (добав­ ление заголовка и форматирование свойства Price объекта модели как денежного значения) оказьmают воздействие и отображаются в браузере (рис. 6.8) . Совет. Процесс подготовки к использованию представлений Razor рассматривается в главе 21.
148 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC С !. <D localhost.:61207 *j:1 ---- --==-----==------ -===--·--::::::::......·-·-! P1·od11cts Nаше Price Lifejacket S48 .95 Soccer ball S19 .50 Сошеr flag $34 .95 L --- ! 1 1 1 ·- --·--__ ) Рис. 6.8 . Результаты внесения изменений в представление Внесение изменений в классы С# Для классов С#, включая контроллеры и модели, способ обработки изменений за· висит от того, как запускается приложение. В последующих разделах объясняются два доступных подхода, которые инициируются посредством разных пунктов в меню Debug и кратко описаны в табл. 6.3. Таблица 6.3 . Пункты меню Debug Пункт меню Start Without Debuggiпg (Запустить без отладки) Start Debuggiпg (Запустить отладку) Описание Классы в проекте компилируются автоматически, когда посту­ пает НТТР-запрос, делая возможной более динамичную практи­ ку разработки . Приложение запускается без отладчика , что не позволяет взять под контроль выполнение кода При таком стиле разработки вы должны явно компилировать проект и перезапускать приложение, чтобы изменения вступили в силу. Во время выполнения к приложению присоединен отлад­ чик, позволяя инспектировать его состояние и анализировать причины возникновения любых проблем Автоматическая компиляция классов В течение нормальной стадии разработки быстрый итеративный цикл позволя­ ет видеть результаты изменений немедленно независимо от того, связаны они с до­ бавлением нового действия или с изменением способа, которым выбираются данные модели представления. Для разработки такого вида Visual Studio поддерживает об­ наружение изменений , как только получен НТГР-запрос из браузера, и автоматичес­ кую перекомпиляцию классов. Чтобы посмотреть, как это работает, выберите пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual Studio. После отображения данных приложения в браузере внесите в контроллер Home изменения, показанные в листинге 6.10.
Глава 6. Работа с Visual Studio 149 Листинг 6.1 О. Фильтрация данных модели в файле HomeCon troller. cs using Microsoft . AspNetCore . Mvc ; using WorkingWithVisualStudio . Models ; using Systern.Linq; narnespace WorkingWithVisua l Studio . Cont r ol l ers puЫic class HorneController : Controller { puЫic IActionResult Index() => View(SirnpleRepository.SharedRepository.Products .Where(p => p.Price < 50)); Изменения связаны с применением LINQ для фильтрации объектов Product, что­ бы представл ению передавались только те из них , свойство Price которых имеет значение меньше 50 . Сохраните изменения, произведенные в файле класса контрол­ лера, и обновите окно браузера, не останавливая или не перезапуская приложение в Vlsual Studio. Отправленный брау з ером НТГР-запрос инициирует процесс компиля­ ции, и приложение будет запущено повторно с использованием модифицированного класса контроллера, генерируя результаты, в которых из таблицы исчез товар Kayak (рис. 6 .9). С) Working with Vi~ual S с Х 11 С [ Ф localhost :6120:_________~ : ----· ---·· - ---------- -- -----·---·------··-- -~- P1·oducts Nаше Price Lifejacket $48.95 J Soccer ball $19 .5 0 1 Соше.1' flag S34.95 '------------------------ _J Рис . 6.9 . Автоматическая компиляция классов Средство автоматической компиляции удобно, когда все идет по плану . Его недо­ статок в том, что ошибки этапа компиляции и времени выполнения отображаются в браузере, а не в Visual Studio, затрудняя выяснение причины возникновения пробле­ мы . В качестве примера в листинге 6.11 демонстрируется добавление ссылки null в коллекцию объектов моделей внутри хранилища. Листинг 6.11 . Добавление ссылки null в файле SimpleReposi tory. cs using Systern . Collections . Generic ; narnespace WorkingWithVisualStudi o . Models puЫic class SirnpleRepository { private static SirnpleRepository sharedRepository = new SirnpleRepository();
150 Часть 1. Введение в инфраструк туру ASP.NEТ Саге MVC private Dictionary<string , Product> products = new Dictionary<string , Product>(); puЫic static SimpleRepository SharedRepository => sharedRepository ; puЫic SimpleRepository() { var initialitems = new[] { }; new Product { Name "Kayak", Price = 275М ), new Product { Name "Lifejacket ", Price = 48 . 95 М }, new Product { Name " Soccer ball", Price 19. 50 М } , new Product { Name "Corner flag", Price = 34 . 95 М ) foreach (var р in initialitems) { AddProduct(p) ; products. Add ( "Error" , null) ; puЬlic IEnumeraЬle<Product> Products => products . Values ; puЫic void AddProduct(Product р) => products . Add(p . Name, р); Средство InteШSense среды Visual Studio будет подсвечивать места в коде, где при­ сутствуют проблемы с синтаксисом. но проблема вроде ссьmки null не будет обнару­ жена вплоть до запуска приложения. Перезагрузка страницы в браузере приведет к тому, что класс SimpleReposi tory скомпилируется, а приложение запустится зано­ во. Когда инфраструктура МVС создает экземпляр класса контроллера для обработки НТГР-запроса от браузера, конструктор HomeController создаст экземпляр класса SimpleReposi tory, 1шторый в свою очередь попытается обработать ссылку null, добавленную в листинге 6.11 . Ссылка null приведет к возникновению проблемы, но сущность проблемы не будет очевидной, потому что браузер не отобразит какое-то полезное сообщение (а браузер Chrome вообще не выведет никаких сообщений. отобразив взамен пустую вкладку). Включение страниц исключений разработчика Во время процесса разработки при возникновении проблемы может быть удобно отображать более полезную информацию в окне браузера. Это можно делать, включив страницы исключений разработчика, что требует изменения конфигурации в классе Startup, как показано в листинге 6 . 12 . Роль класса Startup подробно объясняется в главе 14, а пока достаточно знать, что вызов расширяющего метода UseDeveloperExceptionPage () устанавливает описательные страницы ошибок. Листинг 6.12 . Включение страниц исключений разработчика в файле Startup. cs using Microsoft.AspNetCore . Builder ; using Microsoft.AspNetCore .H osting ; using Microsoft . AspNetCore .Http; using Microsoft . Extensions.Dependencyinjection; using Microsoft.Extensions .L ogging ; namespace WorkingWithVisualStudio {
Глава 6. Работа с Visual Studio 151 puЫic class Startup { puЫic void ConfigureServices(ISe rviceCollection services) { services.AddMvc(); puЫic void Configure(IApplicat ionBuilder арр , IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseDeveloperExceptionPage(); app . UseMvcWithDefaultRoute(); Если вы перезагрузите окно браузера, то автоматически запускаемый процесс компиляции перестроит приложение и выдаст более полезное сообщение об ошибке (рис. 6.1 О). D lnt<rмl Server Error х 1-- --~ ~;~-~host:12~.~~=---=· ~ Ап unhandled exception occшred while processing the request. I NL1 llReferenceExceptioп: Ol)jec 1·eference r.ot set to an insta nc e of an object . <lndex>b_ O _O in HomeCont roller. c:.. line 10 • Que1y Cookies Heade1s NullRefereпceException : Object refere1кe rюt set to ап in sta nce of an object . <lr.·:le<> b_O _C •n Hon1e Cont:ro ller . cs 10. . \olhere(p •> р.Р гiс е < 50)) ; f/ cv~Noxt L~lo·,el·Jext 1r, Index . cs html 17. @foгeach (va1· р in Hodel) ( -~-~~·---·· ------- · ··--·--···---:! Рис . 6.1 О. Страница исключения разработчи ка Отображаемого в браузере сообщения об ошибке может быть достаточно для вы­ явления простых проблем, особенно из-за того, что итеративный стиль разработки означает, что причиной, скорее всего, стали самые последние изменения. Но для уст­ ранения более сложных проблем - и для проблем, которые не становятся очевидны­ ми немедленно - требуется отладчик Visual Studio. Использование отладчика Среда Visual Studio также поддерживает выполнение приложения MVC с примене­ нием отладчика, который позволяет останавливать приложение для инспектирования его состояния и пути прохождения запроса по коду. Это требует другого стиля раз­ работки , поскольку модификации классов С# не будут применены до тех пор, пока приложение не запустится заново (хотя изменения, вносимые в представления Razor, по-прежнему вступают в силу автоматически) .
152 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Такой стиль разработки не настолько динамичен, как использование средства автоматической компиляции, но отладчик Visual Studio великолепен и может предо­ ставлять намного более глубокое проникновение в суть способа работы приложения. нежели то, что возможно с помощью сообщений, отображаемых в окне браузера . Чтобы запустить приложение с применением отладчика, выберите пункт Start Debugging (Запустить отладку) в меню Debug (Отладка) среды Visual Studio. Перед запуском приложения среда Visual Studio скомпилирует классы С# в проекте, но вы можете также вручную скомпилировать свой код, используя пункты меню Build (Постро ение). Пример приложения все еще содержит ссылку null, т.е. необработанное исключе­ ние NullReferen ceExcept i o n, которое генерируется в классе S i mpleReposi tory , прервет приложение и передаст управление выполнением отладчику (рис. 6.11). HomeControПtr.cs ~ Х ~\Vo;.kin9WrthVi$u11IStu~~~p-p,Version_:l~ \4/01ki~~i!_~ViwalStud_i~~~~~m~~~!.~.----·~-----·-,.: B u ~ing ~lic"osoft.д!ipfle-t'Core.Мvc ; li'. lusing Worki ngWithVis ualStudio .М<Юtl::o ; r,-- . ....,.....,.._.,,,...~--:-""::..,,...... ,,. ., __~ -~. -. -- -- ;i using Syste•.linq; /-1.'..-~~~.!!;~ «-~c,epttono<tut~-· _.· _. -- - ~ -- - 2 1 j &:ctpdon thtown: "Sys ttm.Nul\Refe1en ce.f.'щ1ption' in 1 19 n1и1espace Wo гkingWithVi:иJ1йStudio . Cont rollers { ,/ l WoikingW! tliViм'5 tud'ю.dlr 8 p\1Ьli c class 11-)!l'eCм·uol.t~r : Contrclle { / 1 J /' / Additi"onal lnfo~tion: OЬjr.ct rr.ferencl! oot sd to an inst.sn<.t of an objC!.ct.'. •> Vitw( 51ro:rlt'1'to;>cs 1 tor}· · Sh!l':'C!~R!Po~J.~y.Products ! ~~~:~~-· ----------------· · -· · -- 1 puЬlic !1\tticnRc!.iJlt I ndex(} · -· -·- --"-------....,.~- --~ - -- - ..... ··-- ·• -· .lihere(p •> P.·Pric~ < S!)); / f.bШ.!.1?..~~!f!1i!'.!A.!.ti~..o..ЧLts.ti.t!'...Yl!.~.!to!! HM!~ 9Jht..~.~~:;.!bl ~ 1 • t} 1 . Ustt lhe •ntO\'м kt)'lf'.Old tc ctt<!tt dn cbjкt 1nst•nt~ '1 lJ_~_!t'~~~~~lpfo~-:~~~.~~~-·---···-·----~···--··-=-~ ~rch for n1ore Н~р 01'il1ne... 1 "--- --· -- - -- -- - ••" --· -- . " _" - . 1 bc~ptlon ~t1ings-: ! f2J Bruk \.\th~ n tJш rxc.ttp ti11м t).• pe i: thгa 1vn 1д,..;;;-- -- . -- - .. -- --- -·-·- "_,_ _, -·. "----, 1 v""o",;1". j 100о/о. " -i 1 (О р/ e.~ce ptщn detщl to the cl1pbocr1d " ______________________8811!! _ O~nc:c-~~=~tin9~----------------------__J Рис. 6.11. Возникновение необработанного исключения Совет. Если отладчик не перехватывает исключение, тогда выберите пyнктWiпdowsQException Settiпgs (ОкнаQНастройки исключений) в меню Debug (Отладка) среды Visual Studio и удостоверьтесь , что в списке Common Language Runtime Exceptions (Общие исключе­ ния языка во время выполнения) отмечены флажки для всех типов исключений. Настройка точки останова Отладчик вовсе не показывает основную причину проблемы, а только место, где она проявилась . Подсвечиваемый Visual Studio оператор указьшает, что проблема слу­ чилась при фильтрации объектов с применением LINQ, но для получения деталей и выявления лежащей в основе причины потребуется проделать небольшую работу. Точка останова - это инструкция, которая сообщает отладчику о необходимости остановить выполнение приложения и передать управление программисту. Вы може­ те исследовать состояние приложения, посмотреть, что произошло, и при желании возобновить выполнение.
Глава 6. Работа с Visual Studio 153 Чтобы создать точку останова. щелкните правой кнопкой мыши на операто­ ре кода и выберите в контекстном меню пункт Br eakpointqln sert Breakpoint (Точка остановаqВставить точку останова). В целях демонстрации поместите точку останова на метод AddProduct (} в классе SimpleReposi tory (рис. 6.12). o<J V-/ork1n9V11thVi~ualStud10 - Г-'J Х [!) V/orkingWrthVisuaLSt~_dio"NEТCo reApp, Ve r5ion •] ~ \t./crlungW1thViiu~IStudio.Mode:ls. Simple:Reposi • [ 19 Simpl~('.poяtoty{) 1 '$ 1 li~ 1 1 11 • ll, 100 8 .'7 .. puЫic S!mploRopos itory() { vаг initi.slitems • new[ ] { }; ne\'l Pr·oduct { l~a~ • "Kayak" 1 Price " 27S~' } , ne't'1 Product { tlame .- "Lifejacket" 1 Price • 48.95М }, 11ew P rod tн:t { Nam~ • "Soc.ce1· ball" , Price'"' 19.50!'1 } , new Prodщ:t { fJame • ''Coгner flag" , Ргiсе '"' 34.9SМ } foreach (var р in initi.s litems ) { AddProd uct(p) ; pr od ucts . Add( "Error" , null); puЫic П1\ul!'.eг aЫ~<Product> Products "'> products . Vii!l lues ; puЫic 11o id AddP rod uc t(Produtt р) "" > P:ffAМфhfdl!фW3J ; Рис. 6.12. Создание точки остан о ва ' 1• Выберите пункт меню Deb ugc::>Start Debugging (ОтладкаqЗапустить отладку). чтобы запустить приложение, используя отладчик, или Debugc::>Restart (Отладка~::> Перезапустить), если приложение уже выполняется. Во время обработки начального НТГР-запроса от браузера будет создан экземпляр класса SimpleRepository. а вы­ полнение кода достигнет точки останова, где возникнет пауза. В этот момент вы можете применять пункты меню Debug среды Visual Studio или элементы управления в верхней части окна для управления выполнением приложения либо использовать разнообразные представления отладчика, доступные через меню DebugqWindows (ОтладкаqОкна), чтобы инспектировать состояние приложения. Просмотр значений данных в редакторе кода Наиболее распространенное применение точек останова связано с отслеживанием дефектов в коде. Прежде чем можно будет исправить дефект, вы должны выяснить , что происходит, и одним из самых полезных средств, предлагаемых Visual Studio, является возможность просмотра и наблюдения за значениями переменных прямо в редакторе кода. Если вы наведете курсор мыши на аргумент р в методе AddProduct (), подсве­ ченном отладчиком, то появится всплывающее окно, которое показывает текущее значение р (рис . 6 . 13). Рассмотреть всплывающее окно может быть затруднительно, поэтому на рис. 6.13 показана также его увеличенная версия. Результат может не выглядеть особо впечатляющим, т.к. объект данных определен в том же конструкторе, что и точка останова, но это средство работает для любой пе­ ременной. Вы можете исследовать переменные, чтобы увидеть значения их свойств и полей. Правее каждого значения находится небольшая кнопка с изображением кан­ целярской кнопки, которую можно использовать для наблюдения за значением пере­ менной, когда выполнение кода продолжится .
154 Часть 1. Введение в инфраструктуру ASP. NEТ Core MVC Simpl•Repos~ory.cs IOJ"w~rkingWithVis~11IStudio..NETCoreApp.Vers ion " ~ Wo~ki~gWi tl1VisuolStudio .Mo dei~:Si mpl eReposi . ... }; foreaich (vl!lr р in initi alitems ) { 1 nl!\'l Pl"'Qduct { Name • "Corner flag " , Price • 34.9S.Ч } + AddProd uc1:( p); '"s-,,_-p----{W-or-ki-n9-l~-it-hV-is-u•-IS-tu-d-io-.M-o-de-ls-.P-ro-d-uc-.t} о pr-oducts.Add("Errcir" 1 rшl l) ; 1" p.Name Р '" "Koyak" #11 p.P ri ce 275 Gliil р JI p.Name JI p.Pric e {Wo rkingWrthVisualStud io.Mod els. Product} Р •"Кауаk" 275 Рис. 6.1 З. Инспектирование значения данных Наведите курсор мыши на переменную р щелкните на кнопке с изображением канцелярской кнопки, чтобы закрепить ссылку на Product . Разверните закреплен­ ную ссылку. чтобы также закрепить свойства Name и Price, получив в итоге резуль­ тат, представленный на рис. 6 . 14 . lf!J Work 1n 9WithVis uolStud10"NEТCort.App, Vers1on ~ \ otk1ng\V1thV1"u"JStud10.Modf:ls.S1 1npl~Repos1 rФ SimpleR~positoryO new Product { Name • "Corner flag") Price • 34.95r>t } о Рис. 6.14. Закрепление значений в редакторе кода Выберите пункт Continue (Продолжить) в меню Debug (Отладк а) среды Visual Studio, чтобы продолжить выполнение приложения . Поскольку приложение выпол­ няет цикл foreach , снова произойдет пауза, когда точка останова встретится опять. Закрепленные значения показывают, как изменяется объект, присвоенный пер емен ­ ной р . и его свойства (рис. 6.15) .
Глава 6. Работа с Visual Studio 155 G~р JI p.Name /1 p.Price {WorkingWithVisualStudio.Models.Product} Р " "lifejacket" 48.9S Рис. 6.15. Наблюдение за изменением состояния с применением за к реплен н ых значений Использование окна Locals Связанным средством является окно Locals (Локальные) , которое открывается пу­ тем выбора пункта меню Debugr::>Wiпdows r::> Locals (Отладкаr::> Окнаr::>Локальные). В окне Locals отображаются значения данных похожим на закрепленные значения образом, но здесь присутствуют все локальные объекты, относящиеся к точке останова (рис. 6.16) . locals 1 Name " oi this " 1' Producl< ~oi10) ~oi[1] ~ oi Raw Vie\v ~ !M!!fi!!.ijЩi!!Wi ~ "1: Static members Value {WorkingWit hVisualStudio .M odels.SimpleRepo sitory) Count = 2 {WorkingWithVisualStudio.Models.Product) {\"lorkingWithVisualStudio.Mod<Ols.Product} • 1:1х ; Туре_ __ • _ ··- WorkingWithVisualStudio.Mode ls.Sim pleR epos Syste m.Colfecticns.Generic .I EnumeraЫe<Work WorkingWitl1VisualStudio.Mode ls.P roduct Wo rki ngWithVisualStudio .Model s. Product Sy;tem.Colfectrons.Generic.D1ct1щшy<5tring, i " " р1' Nan10 {WorkingWit hVi sualStudio.Mod els. Product} · soccerЬа11· WorkingWit hVisualStudio.Mod el<.Product Q. • string 1' Price 19.SO decimal Рис . 6.16. Окно Locals Каждый раз, когда вы выбираете пункт Continue , выполнение приложения возоб­ новляется и в циюrе foreach обрабатывается новый объект. Продолжая поступать так, вы увидите ссылку null в окне Locals и в закрепленных значениях, отображае­ мых внутри редактора кода . Применяя отладчик для управления выполнением при­ ложения, вы можете отслеживать его продвижение по коду и получить представление о том, что происходит. Устранить проблему со ссылкой null можно было бы за счет очистки коллекции объектов Product, но альтернативный подход предусм атривает увеличение надеж­ ности контроллера, как показано в листинге 6.13, где для проверки на предмет зна­ чений null используется null -условная операция (описанная в главе 4). Листинг 6.13. Исправление проблемы со ссылкой null в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using WorkingWithVisualStudio . Models; using System . Linq ; namespace Wo rkingWithVisualStudio.Controllers puЬlic class HomeController : Controller { puЫic IActionResult Index() = > View(SimpleRepository.SharedRepository . Products .Where(p => p?.Price < 50));
156 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Отключить точку останова можно, щелкнув правой кнопкой мыши на операто­ ре кода, где она находится, и выбрав в контекстном меню пункт Breakpoint~Delete Breakpoint (Точка останова<=:> Удалить точку останова). Перезапустив приложение, вы увидите простую таблицу с данными (рис. 6.17). [:) Working with Visu•I Stu, Х · С [Ф~·;h:;s~;~1 207 ·---- ----· - -----;-] ·----·- ------ -· -~· ·-·- P1·o<l11cts Nа ше Pri ce Lifejackc r $48. 95 L Socce1· bal\ $19.50 Сошеr flag $34 .95 ----· --·---------------1 Рис. 6.17 . Устранение дефекта По сравнению с задачами, которые требует реальное выявление ошибок, эта про­ блема была решена просто, однако отладчик Visual Studio чрезвычайно эффективен, и с помощью многочисленных доступных представлений приложения и управления вы ­ полнением вы можете легко углубляться в детали для выяснения, что пошло не так. Использование средства Browser Link Средство Browser Link (Ссылка на браузер) позволяет упростить процесс разра­ ботки за счет помещения одного или большего числа браузеров под контроль Visual Studio. Оно особенно полезно, если нужно видеть влияние изменений в некотором диапазоне браузеров. Средство Browser Link работает с или без отладчика, но оно наиболее полезно в случае применения средства автоматической компиляции клас­ сов. потому что появляется возможность модифицировать любой файл в проекте и видеть влияние изменения, не переходя в окно браузера и не перезагружая страницу вручную. Настройка средства Browser Link Включение средства Browser Link означает добавление в проект еще одной сборки и изменение его конфигурации. В листинге 6.14 демонстрируется добавление сборки Mi cro s oft . VisualStudio. Web . Browse rLink. Loader в раздел dependencies фай­ лаproject.json. Листинг 6.14. Добавление сборки Browser Link в файле proj ect. j son " dependencies ": { " Microsoft .N ETCore.App ": "version": "1.0.О", " type ": " platform " }' " Microsoft . AspNetCore.Diagnostics ": " 1 . 0 . 0 ",
}' Глава 6. Работа с Visual Studio 157 " Microsoft . AspNetCore.Server . IISintegration ": "1 . 0 . О" , "M icrosoft . AspNetCore . Server. Kes trel 11 : "1.О.О 11 , " Microsoft .Ex tens i ons .Logging.Console ": "1. 0 .О", "Microsoft .AspNetCore . Mvc ": "1. О . О ", 11 Мicrosoft.VisualStudio.Web.BrowserLink.Loader": "14.0 .0" В листинге 6.15 показано соответствующее изменение в классе Startup. Листинг 6.15. Включение средства Browser Link в файле Startup. cs using Microsoft . AspNetCore . Builder; using Microsoft . AspNetCore . Hosting ; using Microsoft . AspNetCore . Http ; using Microsoft.Extensions .De pendencyin jection ; using Microsoft . Extensions .Lo gging ; namespace WorkingWithVisualStudio puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc(} ; puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env , ILoggerFactory loggerFactory) { app . UseDeveloperExcept i onPage(); app.UseBrowserLink(); app.UseMvcWithDefaultRoute(); Использование средства Browser Link Чтобы понять, как работает средство Browser Link , выберите пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual Studio. Среда Visual Studio запустит приложение и откроет новую вклад1<у в окне браузера для отоб­ ражения результатов . Просмотрев НТМL-разметку , отправленную браузеру, вы заме­ тите, что она содержит дополнительный раздел, подобный приведенному ниже: < ! DOCTYPE html> <html> <head> <meta name= 11 viewport 11 content= " width=device-width " /> <title>Working with Visual Studio</title> </head> <body> <h3>Products</h3> <tаЫе> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> <tbody>
158 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC <tr><td>Lifejacket</td><td>$48 . 95</td></tr> <tr><td>Soccer ball</td><td>$19 . 50</td></tr> <tr><td>Corner flag</td><td>$34 . 95</td></tr> </tbody> </tаЫе> < ! - - Visual Studio Browser Link --> <script type="application/json" id=" browserLink initializationData"> - - {"requestid":"9eOOcбfB05Bf436981Be7ba315c9bdde", "requestмappingFromServer" : false} </script> <script type="text/javascript" src="http://localhost:56147/e7bB5fe070c5419Ba04ld57c363cee40/browserLink" async="async"></script> < ! -- End Browser Link --> </ body> </html > Среда Visual Studio добавляет в НТМL-разметку , посылаемую бр аузеру, пару эле ­ ментов script , которые применяются для открытия долговечного НТГР-подключения к серверу приложений, чтобы ср еда Visual Studio могла обеспечить принудител ьную перезагрузку страницы браузером. (Если вы не видите элементы script, тогда удос­ товерьтесь в том, что отмечен пункт ЕnаЫе Browser Link (Включить Browser Link) в меню, показанн ом на рис. 6 . 18.) В листинге 6.16 приведено изменение, внесенное в представл ение Index, которое проиллюстрирует результат использования средства Browser Link. .1ninistrator) р t:! х "ools Test Дnalyze Window Help Adam Freeman • 11 Any CPU . ~ ' • llS Express • ·~~ • ' [ ·ff ~-;j[__~ i:f!.L'~"~J~_!_.:!.:i! .:L . . .. ._ ___. G Refresh Lin ked Browsers ~ Ctrl+Alt. - Enter . .S ~@} BromerlinkOashboard 1 ,~ ЕnаЫе Browser Link " ЕnаЫе CSS Auto·Sync - - --"...,.--- ... " = Solution ltem s 61 global.json Рис. 6.18. Применение средства Browser Link для перезагрузки браузера Листинг 6.16. Добавление временной отметки в файле Index. cshtml @model IEnumeraЫe<WorkingWithVisualStudio.Models.Product > @{ Layout = null; } < !DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>Working with Vi sua l Studio</titl e> </head>
<body> <h3>Produ c ts < /h3 > Глава 6. Работа с Visual Studio 159 <p>Request Time: @DateTime .Now. ToString ("HH:mm: ss") </р> <tаЫе> <thead> <tr><td>Name</td><td>Pri ce</td></ tr> </thead> <tbody> @foreach (var р in Model) <tr> <td>@ p. Name</td> <td>@( $" {p . Price : C2) " )</td> </tr> </tbody > </tаЫе> </body> </html> Сохраните изменение в файле представления и выберите пункт Refresh Linked Browsers (Обновить связанные браузеры) в меню средства Browser Link внутри панели инструментов Visual Studio (см. рис. 6.18). (Если средство Browser Link не работает, попробуйте перезапустить Visual Studio и повторить попытку.) Код JavaScript, встроенный в отправляемую браузеру НТМL-разметку, будет пере­ загружать страницу, де монстрируя результат добавления , которым является появле­ ние простой временной отметки. Каждый раз , когда выбирается пункт меню Refresh Linked Browsers , браузер будет делать новый запрос к серверу. Результатом запроса будет визуализация представления Index и генерирование новой НТМL-страницы с обновленной временной отметкой. На заметку! Элементы scr i p t, относящиеся к средству Browser Link, встраиваются толь­ ко в успешные ответы. Это значит, что если во время компиляции класса , визуализации представления Razor или обработки запроса возникает исключение , то подключение меж­ ду браузером и Visual Studio утрачивается и вам придется перезагружать страницу, ис­ пользуя сам браузер, пока проблема не будет устранена. Использование нескольких браузеров Средство Browser Link может применяться для отображения приложения в не­ скольких браузерах одновременно, что удобно, когда нужно уладить вопросы несходс­ тва реализаций между браузерами (особенно при реализации специальных таблиц стилей CSS) или увидеть, как приложение визуализируется набором настольных и мобильных браузеров . Чтобы отобрать браузеры, которые будут использоваться, выберите пункт Browse With (Просмотреть с помощью) в меню, связанном с кнопкой llS Express в панели инс­ трументов Visual Studio (рис. 6.19). Среда Visual Studio отобразит список известных ей браузеров. На рис. 6.20 пока­ заны браузеры, установленные в моей системе; часть из них установлена вместе с Windows (lnternet Explorer и Edge), а часть - лично мною по причине их широкого распространения.
160 Часть 1. Введение в инфраструктуру ASP.NET Core MVC '' ~D fr ~Q;ii~k L~unf llSExpr~s . / 115 Express WorkingWithVi.sualStudio th• device-..,1idth" /> 'title> Web Bro,vstr (Google Chrome Canary) Brows.Wrth" . Рис. 6.19. Выбор множества браузеров Bro wse rs (select one or more): Google Ch rome Ca nary lnt erna l Web Browse r Opera lnt ern et Br owse r Ptogram: Size of browse r window: [---· --·· ·---] Set as Default .;,.~~......- - - - Рис. 6.20. Отбор браузеров из списка ' Среда Visual Studio ищет общие браузеры во время процесса установки, но с по­ мощью кнопки Add (Добавить) вы можете указывать браузеры, которые не были об­ наружены автоматически. Вы можете также настроить инструменты тестирования от независимых поставщиков наподобие Browser Stack, которые запускают браузеры на расположенных в облаке виртуальных машинах, так что вам не придется управлять крупной матрицей операционных систем и браузеров во время тестирования. На рис 6.20 выбраны три браузера: Chrome, Internet Explorer и Edge . Щелчок на кнопке Browse (Обзор) приводит к запуску всех трех браузеров и заrрузки в них URL примера приложения (рис . 6.21). Чтобы посмотреть , какими браузерами управляет средство Browser Link , выберите пункт Browser Link Dashboard (Инструментальная панель Browser Link) в меню средс­ тва Browser Link; откроется окно Browser Link Dashboard (Инструментальная панель Browser Link) , представленное на рис. 6.22. Инструментальная панель показывает URL, отображаемый каждым браузером , и позволяет обновлять каждый браузер по отдельности.
Глава 6. Работа с Visual Studio 161 EJ Worki ng with Visua1 Stu 1 Х 11\ttp.://l ocilhost:60 )1 8/ ~ 'Norr. in9 ""ilh Visu1'.IStvd- Х · I ~· -) () 1 l0<.1 ~••:: _ • i} ..O:. .~~~~ait1 ~~~~~18~==~:::.~-=--=---=-~ S R•questTime: 17:5 1:26 Pi·odurts 1 P1·oducts 1 Name P rice 11 Nasne Pri ce 1 ReqнestT11110: 17:51.26 Lifejacke t !:48.95 Soccer ball .Е19.50 Lif•jocke t f 4&. 95 1 No111е Price ' Corne r ftag 1:34 .95 \ Soccer ball П9.50 L1 f<;ncket S4S .95 L ·-. ___ --·----- ___ __ j Corner fl ag 04.95 Sосш bnll $19 .5 0 j Сошеr flag $34.95 1 t--·" - -------------·---------·~ --~-- · -- - -- -- Рис. 6.21 . Работа с несколькими браузерами Browser Link Dash board .А \1,'o rkingWithVisua lStudi o (0 co nne ctions) .А Co nnectio ns 1·...zo c ur re"t~onnec 1ons View in Brow~er .А U nkno\'IП (З con nections) .А Connections Chrome • http:/ /l o cal h ost6051 8f Microsoft Edge ... htt p;//l o c a l hostб0518/ Mozilla ... htt p:// localhost60518/ Learn n1ore about Bro\'ISEI Link Рис. 6.22 . Окно Browser Link Dashboard Подготовка файлов Java Script и CSS для развертывания При создании клиентской части веб-приложения вы обычно будете создавать ряд специальных файлов JavaScript и CSS, которые дополняют аналогичные файлы в па­ кетах, установленных инструментом Bower. Такие файлы требуют специальной обра ­ ботки с целью оптимизации их доставки в производственную среду, чтобы миними­ зировать количество НТГР-запросов и долю полосы пропускания , необходимую для доставки файлов клиенту. В этом разделе будет описано расширение Visual Studio, которо е Microsoft пр едоставляет для выполнения указанной задачи . Включение доставки статического содержимого Инфраструктура ASP.NET Core располагает поддержкой для доставки статических файлов из папки wwwroot клиентам , но когда проект созда ется с применением шаб­ лона Empty, упомянутая поддержка по умолчанию отключена. Чтобы включить подде­ ржку статических файлов , в раздел dep e nde nci es файла p roj ect . j s on потребуется добавить новый пакет (листинг 6 . 17) .
162 Ч асть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 6.17 . Добавление пакета в файле project. json " dependencies ": { "Microsoft . NETCore . App ": "version": "1 .0 .0", " type ": " platform " }' }' " Microsoft . AspNetCore . Diagn ostics ": " 1 .0 .0", " Microsoft . AspNetCore . Server.IISintegration ": " 1.0 .0 ", "Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ", " Microsoft . Extensions . Logging . Co nsole ": " 1 . 0 .О", "Microsoft.AspNetCore .Mvc": "1 .О . О", " Microsoft . VisualStudio . Web .BrowserLink . Loader": " 14 . 0 . 0 ", "Microsoft . AspNetCore.StaticFiles": "1.0.0" Пакет Microsoft . AspNetCore . Stat icFiles содержит функциональность для обработки статических файлов, которая должна быть включена в кла с се Startup, как показано в листинге 6.18. Листинг 6. 18. Включение поддержки статических файлов в файле S tartup . cs using Microsoft . AspNetCore.Builder ; usi ng Microsoft . AspNetCore .H osting ; using Microsoft . AspNetCore . Http; using Microsoft . Extensions.Dependencyinjection ; using Microsoft.Extensions.Logging; namespace Work i ngWithVisualStudi o puЫ i c class Startup { puЫic void ConfigureServices(I ServiceCollection services) { services . AddMvc() ; puЬlic void Configure(IApplicationBui l der арр , IHostingEnvironment env , ILoggerFactory l oggerFactory) { app . UseDeveloperExceptionPage() ; app . UseBrowserLink(); app.UseStaticFiles(); app .U seMvcWithDefaultRoute() ; Добавление в проект статического содержимого Чтобы продемонстрировать процесс пакетирования и минификации, пон адобится добавить в проект какое-нибудь статическое содержимое и обеспечить возможность его доставки клиенту. Для начала создайте папку wwwroot/css, где будут храниться специальные файлы CSS. Затем добавьте файл по имени first . css, используя шаб ­ лон элемента Style Sheet (Таблица стилей), как показано на рис. 6 .23.
ASP.NEТ Cli,e.nt-~e Code ~ Online Глава 6. Работа с Visual Studio 163 TypeSc:tipt JSX File Cli~nt·side. TypeScript JSON Configuraticn File Client ·side- Bo\VtrConfigu rc!tion file Cl1ent-side npm Con figuration Fi!e ( lient· side N.ame first .css Рис. 6.23. Создание таблицы стилей CSS Поместитевфайл first .сss стилиCSSизлистинга6.19. Листинг 6.19. Содержимое файла f1rs t. css из папки wwwroot/ cs s hЗ{ font- size: 18 pt; font - family: sans - serif ; tаЫе, td { border : 2рх solid Ыасk; border - collapse : collapse ; padding : 5рх; font - family: sans - serif ; Повторите процесс для создания в папке wwwroot/css еще одной таблицы стилей по имени second. css с соде ржимым , прив еденным в листинге 6.20. Листинг 6.20. Содержимое файла second. css из папки wwwroot/css р{ font - family: sans -se rif ; font- size: 10 pt; color: darkgreen ; background-color : antiquewhite ; border : lpx solid Ыасk ; padding : 2рх ; Специальные файлы JavaScript хранятся в папке wwwroot/j s . Создайте эту папку и с применением шаблона элемента JavaScript File (Файл JavaScript) добавьте в нее файл third . j s (рис . 6.24).
164 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC ' ASP. NEl ~client·sid~ (ode " Туре: Client·side f А script fi le co nt ai ning JlJvllScri pt со ~ Onl inf Nllme: f;J f;J Type:S cr ipt File Type Script JSX File TypeSc ript JSO N Co nf igurati on File Bowe r Co nfi gura t1on File Client·sid~ Client·side Clie nt· side Client-~ide Click here. to $19 0 11line an d find t~ third .js 1 { -----------~-~-----·--"-~-·----ц Рис. 6.24. Создание файла JavaScript Поместите в новый файл простой код JavaScript, представленный в листинге 6.21. Листинг 6.21 . Содержимое файла third. j s из папки wwwroot/ j s document . addEventListener( " DOMContentLoaded ", function () { var element = document . createElement("p " ) ; element.textContent = "This is the element from the third .js file "; document . querySelector( "body " ) . appendChild(element) ; )); Нам необходим еще один файл JavaScript. Создай те в папке wwroot/js файл по имени fourth. j s с содержимым , показанным в листинге 6.22. Листинг 6.22 . Содержимое файла fourth. j s из папки wwwroot/ j s document.addEventListener("DOMContentLoaded", function () { var element = document . createElement ( "р"); element. tex tContent = " This is the element fr om the fourth. j s file "; document . querySelector( " body " ) . appendChi l d(element) ; }); Обновление представления Последний подготовительный шаг связан с обновлением представления Index. cshtml для использования новых таблиц стилей CSS и файлов JavaScrlpt (листинг 6.23). Листинг 6.23. Добавление элементов script и link в файле Index. cshtml @model IE numeraЬle<WorkingWithVisua l Studio . Models . Produc t > @{ Layout = null; )
< !DO CTYPE html> <html> <head> Глава 6. Работа с Visual Studio 165 <meta name="viewport" content= " width=device - width" /> <title>Working with Visual Studio</title> <link rel="stylesheet" href="css/first.css" /> <link rel= "stylesheet" href="css/second.css" /> <script src="js/third . js"></script> <script src="js/fourth. js"></script> </head> <body> <h3>Products < /h3> <p>Request Tim e : @DateTime . Now . ToString("HH:mm:ss")</p> < tаЫе> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> <tbody> @foreach (var р in Model) <tr> <td>@p . Name</td> <td>@($ " {p.Price : C2} ")</td> </tr> } </tbody> </tаЫе> </body> </html> Запустив пример приложения, вы увидите содержимое, приведенное на рис. 6.25. Существующее содержимое было стилизовано посредством таблиц стилей CSS, а код JavaScript добавил новое содержимое. D \'Jorking V1ith Vi'u•I Stu· Х ---· ___с _ СФ:~~~:;~~~~=~-~-::..---::::~~-=--=====----_-::::'____~-----~~- :=-~~-- ! \ Products l /Request Time: 17'29: 48 Name Pri ce Lifejacket $48 .95 Socc er ball $19 .50 Corner flag $34 .95 1 1~,Th-l-sl_s_th-e-el-em_en_t_r_ro-,n-the-th-lro-.J-s-fi~---------------------~111 ~his is the elemer.1 fron1 the fourth.js file . Рис . 6.25. Выполнение примера приложения
166 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Пакетирование и минификациS1 в приложениS1х MVC В данный момент есть четыре статических файла, и браузер должен делать четы­ ре запроса, чтобы получить эти статические файлы. При доставке клиенту каждый такой файл отнимает больше полосы пропускания. чем должен. поскольку содержит пробельные символы и имена переменных, которые являются содержательными для разработчика, но не имеют никакого значения для браузера . Объединение файлов одного и того же типа называется пакетированием. Уменьшение размеров файлов называется минификацией . Обе задачи выполня­ ются в приложениях ASP.NET Core MVC с помощью расширения Bundler & Minifier (Упаковщик и минификатор) для Visual Studio. Установка расширения Visua/ Studio Первый шаг заключается в установке расширения. Выберите пункт меню Toolsc::>Extensi o ns and Updates (СервисqРасширения и обновления) и щелкните на кате­ гории On line (Онлайновые), чтобы отобразить галерею доступных расширений Visual Studio. В поле поиска справа вверху введите строку Bund ler & Mini fie r (рис . 6.26). Найдите расширение Bundler & Minifler и щелкните на кнопке Download (Загрузить). чтобы добавить его в Visual Studio. Завершите процесс установки и перезапустите Visual Studio. ~ lnstolled " o~itne:·. ,,. '' • Visual Studio G•lfery ~ Controls ~ Templates • Tools !Seatc ~ults ~ Samples Gallery Р Updotes (2) Web E.ssentin ls 2015.3 Adds many us~ ul features to \/i<J ual Studio for we b develop•" · Requires Visu•I Studio 2015 Web Exton.slon Pack The ea~1est way to set up Visual Studto for tl't ultimtte we:b dNelopment experienc e. Web An;ilyzer Provid•s static analysis dir~ctly in Visual St udio for JavdScript, Type:Sc ript, JSX. CSS .Jnd mo re ..t;J lmng" Sprites 1 ~" ~1 ,,. Bundler & Minifier х• Create d Ьу: Mods Kristensen ~ Versio n: 2. 1.255 ~ Oo>Vnload s: 144S40 Rating: ., (42 Votes) ' More lnformat1on 1 Repo rt Exte n:-.ion to Mкrosoft ~,,, 1 ~ ~у Close ' ' ' ..:...~....._._.".:....._:....:;...~".-;....~~--·....;,._.-··-·· -·~·~·---,.,.:_ ... _;. .;. , '__ . .. . .. .. . .. .. . . . -: .. _'~..... "-..;.·-~.:;·-·:.;_,-..:.~:..~-~-·~-----' Рис. 6 .26 . Поиск расширения Visual Studio пакетирование и минификация файлов После установки расширения и перезапуска Visual Studio вы можете выбирать множество файлов одного типа, пакетировать их вместе и минифицировать их содер­ жимое. В качестве примера выберите файлы first. css и second. css в окне Solution Explorer, щелкните на них правой кнопкой мыши и выберите в контекстном меню пункт Bundler & MinifierqBundle and Minify Files (Упаковщик и минификаторqПакетировать и минифицировать файлы). как показано на рис . 6.27.
Solution Explorer • r:3Х О'IФ ~ ! Ф·~@!@ с+ Open . . Se6rch Solut• on Ex~or (Ctrl• :.! ._ Opo n With". Ф ~JWroot Hide from So lution Exp lorer . @] fors\.css. ·,_.,. ," ~ second.;;s " -.ijs rr fourth.js rr th iгd .js lib {} Cle•nup Selocted Code Coll.apse Recurs1vely - . - - ~ Rt1n Web Code: Analysi.s -· . --..~- . Bundler & Minifitr .. .. NEW Solution Explore.r V1e.;,v J(, Cut ~--------< Q1 Сору Х D•lete Rfnan1t 1' Properties Ctrl+X Ct rl+C Del Aft•Enter Глава 6. Работа с Visual Studio 167 Рис. 6.27. Пакетирование и минификация файлов CSS Сохраните выходной файл как bun dle. c ss и расширение обработает файлы CSS. В окне Solution Explorer отобразится новый элемент b undl e . cs s, который можно рас­ крыть и увидеть минифицированный файл по имени bund l e . min. css . Открыв ми­ нифицированный файл, вы заметите, что содержимое обоих отдельных файлов CSS было объединено, а все пробельные символы удалены. Работать с этим файлом на­ прямую вы вряд ли захотите, однако он меньше по размеру и требует только одного НТТР-подключенил для доставни стилей CSS клиенту. Повторите процесс с файлами third. j s и fo u rth . js , чтобы создать новые фай­ лыbundle.jsиbundle.min .jsвпапке wwwroot/js. Внимание! Удостоверьтесь, что выбираете файлы в порядке, в котором они загружаются браузером , для того чтобы предохранить порядок следования стилей или операторов кода в выходном файле. Таким образом, например, выбирайте файл thi rd. j s перед файлом four th. j s , чтобы обеспечить выполнение кода в правильном порядке. В листинге 6.24 показано представление I ndex . csht ml, в котором элементы link для отдельных файлов заменены одним таким элементом , запрашивающим пакетиро­ ванные и минифицированные файлы. Листинг 6.24 . Применение пакетированных и минифицированных файлов в файле Index. cshtml @model IEnumeraЫe<WorkingWithVisualStudio .Models. Product> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name= "viewport" cont ent="wi dt h=devi ce- width" /> <title>Working with Visual Stud io</ti tl e> <link rel="stylesheet" href="css/bundle.min.css" /> <script src="js/bundle .min. js"></script> </head> <body> <hЗ>P r oducts</hЗ> <p>Request Time: @DateTime .Now.ToString("HH:mm:ss")</p>
168 Часть 1. Введение в инфраструктуру ASP.NET Core MVC <tаЫе> <thead> <tr><td>Name</td ><td >P ri ce</td></ tr> </thead> <tbody> @foreach (var р in Model) <tr> <td>@ p.Name</td> <td>@($ "{p. Price: C2 }" )</ td> </tr> </tbody> </tаЫе> </body> </html> После запуска приложения вы не заметите никаких визуальных отличий, но оно использует пакетированные и минифицированные файлы, пр едоставляя браузеру все стили и код, которые были определены в отдельных файлах. По мере выполнения оп ераций пакетирования и минификации расширени е в едет учет обработанных файлов в файле по имени b un d l econfig . j son, находяще мся в корневой папке проекта. Вот конфигурация. которая была сгенерирована для файлов в примере приложения: " outputFileName": " wwwroo t/c s s /bundle. c ss", "inp utFile s": [ " wwwroot/ c ss/first . css ", " wwwroo t /css/second . cs s" ] }1 { " outputFileName ": " wwwroot/ j s/bundl e .j s ", " inputFiles ": [ " wwwroo t / j s/third .js", "wwwroot/js/fourth.js" Расширение автоматически отслеживает входные файлы на предмет изменений и за­ ново генерирует выходные файлы, когда происходят изменения. гарантируя отражение в пакетированных и минифицированных файлах результатов любого редактирования. Для демонстрации в листинге 6.25 приведено изменение. внесенное в файл thi rd . j s. Листинг 6.25. Внесение изменения в файл third. j s docume nt . addEvent List ener ("DOMContentLoaded", f unct ion () { var el ement = document . createElement ("р") ; element.textContent = "This is the element from the (modified) third . js file"; document . querySelector( " body " ) . append Ch ild(e l ement) ; });
Глава 6. Работа с Visual .Studio 169 После того как файл сохранен, расширение заново сгенерирует файл bundle . min. j s. Перезагрузив страницу в браузере , вы увидите изменение (рис . 6.28). Name Price Lifejacket $48 .95 Socce r ball $ 19.50 Corne r flag $34.95 Рис. 6.28 . Обнаружение изменений в пакетированных и минифицированных файлах Резюме В этой глав е была рассмотрена структура проектов МVС, описаны две доступные исполняющие среды .NET и обсуждены средства, которые Visual Studio предлагает для разработки веб-приложений, включая автоматическую компиляцию классов, средство Browser Liпk, а также пакетирование и минификацию. В следующей главе объясняет­ ся. как проекты ASP.NET Core МVС подвергаются модульному тестированию .
ГЛАВА 7 Модульное тестирование приложений МVС в настоящей главе будет показано, как проводить модульное тестирование при­ ложений MVC. Модульное тестирование - это вид тестирования, при котором индивидуальные компоненты изолируются от остальной части приложения, позволяя тщательно проверить их поведение . Инфраструктура ASP.NET Core МVС была спроек­ тирована так, чтобы сделать легкой задачу создания модульных тестов, и среда Visua] Studio предлагает поддержку для широкого диапазона инфраструктур модульного тестирования. В главе объясняется , как настроить проект модульного тестирования, каким образом установить одну из самых популярных инфраструктур тестирования и что собой представляет процесс написания и прогона тестов. В табл. 7.1 приведена сводка по главе. Таблица 7.1. Сводка по главе Задача Создание модульного теста Изоляция компонентов для модульного тестирования Прогон тех же самых тестов xUnit.net с разными значениями данных Упрощение процесса создания фиктивных тестовых объектов Решение Листинг Создайте проект модульного тестирования, 7.1-7.8 установите тестовый пакет и добавьте клас- сы, которые содержат тесты Используйте интерфейсы для разделения 7.9 -7 .16 компонентов приложения и применяйте фиктивные реализации с ограниченными тестовыми данными в модульных тестах Используйте параметризованный модуль­ ный тест либо получайте тестовые данные из метода или свойства Используйте инфраструктуру имитации 7.17-7 .19 7.20-7 .22
Глава 7. Модульное тестирование приложений MVC 171 Проводить ли модульное тестирование? Наличие возможности легко выполнять модульное тестирование является одним из преиму­ ществ использования инфраструктуры ASP.NET Core MVC, но эта процедура не для всех, и я не намерен притворяться, что дело обстоит иначе. Мне нравится модульное тестирование, и я применяю его в своих проектах, но далеко не во всех и не настолько согласованно, как вы могли ожидать. Я предпочитаю концентрироваться на написании модульных тестов для средств и функций, о которых знаю , что они будут труд­ ны в реализации и, скорее всего, станут источником ошибок после развертывания. В таких ситуациях модульное тестирование помогает структурировать мои мысли о том , как лучше всего реализовать то, что необходимо. Я считаю, что одно лишь размышление о том, что нужно тестировать, способствует появлению идей относительно потенциальных проблем, причем до того, как придется иметь дело с действительными ошибками и дефектами. Тем не менее, модульное тестирование - это инструмент, а не догма, и только лично вы знаете, в каком объеме должно проводиться тестирование. Если вы не находите модульное тестирование полезным или располагаете другой методологией, которая вам больше под­ ходит, то не должны думать, что вы обязаны использовать модульное тестирование просто потому, что это модно. (Однако если вы не располагаете более эффективной методологией и вообще не выполняете тестирование, то вероятно даете возможность пользователям об­ наруживать имеющиеся ошибки, что редко оказывается идеальным решением . Вы не обяза­ ны применять модульное тестирование, но действительно должны проводить тестирование какого-то вида.) Если вы не сталкивались с модульным тестированием ранее, я предлагаю опробовать его и посмотреть, как оно работает. Если вы не являетесь поклонником модульного тестирования, то можете пропустить данную главу и перейти к чтению главы 8, где будет начато построе­ ние более реалистичного приложения MVC . П одготовка проекта для примера В этой главе мы продолжим пользоваться проектом Wor k i ngW i thVisu al S t udi o , который был создан в главе 6. Здесь мы добавим в него поддержку для создания но­ вых объ е ктов P r oduc t в хранилище. Включение встроенных дескрипторных вспомогательных классов В данной главе применяется один из встроенных дескрипторных вспомога­ тельных классов для установки атрибута href элемента а. Работа дескрипторных вспомогательных классов подробно объясняется в главах 23-25, а здесь мы просто должны включить их. Создайте файл импортирования представлений, щелкнув пра­ вой кнопкой мыши на папке Vi ews, выбрав в контекстном меню пункт Addi::>New ltem (Добавить~::> Новый элемент) и указав шаблон элемента MVC View lmports Page (Страница импортирования представлений МVС) из категории ASP.NET . Среда Visual Studio автоматически назначит файлу имя _ Vi e wi mp ort s. cs html, а щелчок на кнопке Add (Добавить) приведет к его созданию. Поместите в файл содержимое, по­ казанное в листинге 7. 1 . Листинг 7.1 . Содержимое файла_Viewimports. cshtml из папки Views @addTagHelper *, Mi crosoft.AspNetCore .Mvc .TagHelper s
172 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Этот оператор включает встроенные дескрипторные вспомогательные классы. в том числе тот, который вскоре будет использоваться в представлении Index . Можно было бы добавить операторы using для импортирования пространств имен из проек­ тов, но представления не являются важной частью рассматриваемого в данной главе примера приложения, так что ссылка на типы моделей с их пространствами имен не вызовет проблем. Добавление действий к контроллеру Первый шаг связан с добавлением действий к контроллеру Home, который будет визуализировать представление для ввода данных и для получения этих данных от браузера (ли стинг 7.2). Действия следуют тому же шаблону, который применялся в главе 2 и подробно обсуждается в главе l 7. Листинг 7.2. Добавление методов действий в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using WorkingWithVisualStudio . Mode ls; using System.Linq; namespace WorkingWithVisua lStudio.Controllers puЫic class HomeController : Controller { SimpleRepository Repository = SimpleRepository.SharedRepository; puЬlic IActionResult Index () => View (Repository. Products .Where (p => p?.Price < 50)); [HttpGet] puЬlic IActionResult AddProduct () => View (new Product () ) ; [HttpPost] puЫic IActionResult AddProduct(Product р) Repository.AddProduct(p); return RedirectToAction("Index"); Создание формы для ввода данных Чтобы снабдить пользователя возможностью создания нового товара, мы создадим представление Razor по имени AddProduct . cshtml в папке Views/Home . Соглашения относительно имени файла и его местоположения соответствуют стандартному пред­ ставлению, которое визуализирует метод AddProduct () контроллера Home . В лис­ тинге 7 .3 приведено содержимое нового представления, которое полагается на пакет Boostrap, добавленный к проекту с использованием Bower в главе 6. Листинг 7.3. Содержимое файла AddProduct. cshtml из папки Views/Home @model WorkingWithVisualStudio.Models.Product @{ Layout = null; } < !D OCTYPE html> <html> <head>
Глава 7. Модульное тестирование приложений MVC 173 <meta name= " viewport " content = "width=devi ce - wi dth " /> <title>Working with Visual Studio</title> <link rel= " stylesheet " href= " /liЬ/bootstrap/dist/css/bootstrap . min . css " /> </head> <body class= " panel - body " > <hЗ>Create Product</hЗ> <form asp- action= "A ddProduct " method= "post " > <div c l ass= " form-group " > <label asp - for= " Name">Name : </label> <input asp - for= "N ame " class= " form -control " /> </div> <div class= " form - group " > <label asp -for= " Price " >Price:</label> <input asp - for= " Price " class= " form - control " /> </div> <button type="submit" class =" Ьtn Ьtn -pr imary " > Add </button> <а asp-action= " Index " class= " Ьtn Ьtn -de fault " >Cancel</a> </form> </body> </html> Представление содержит НТМL-форму, которая применяет НТТР-запрос POST для отправки значений Name и Price действию AddProduct контроллера Ноте . Содержимое стилизовано с использованием пакета CSS из Bootstrap. Обновление представления Index Последний подготовительный шаг предусматривает обновление представления Index , чтобы включить в него ссылку на новую форму (листинг 7 .4). Кроме того, поя­ вилась возможность удалить файлы JavaScript, используемые в предыдущей главе, и заменить специальные таблицы стилей CSS стилями Bootstrap, которые применяют­ ся к элементам HTML в представлении. Листинг 7.4 . Обновление содержимоrо в файле Index. cshtml @model IEnumeraЫe<WorkingWithVisualStudio . Models.Product> @{ Layout = null; } < ! DOCTYPE html> <html> <head> <meta name:o " viewport " content= " width=device - width " /> <title>Worki n g with Visua l Studio</title> <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.min. css" /> </head> <body class="panel-body"> <hЗ class="text-center">Products</hЗ> <tаЫе class="taЫe taЬle-bordered taЫe-striped"> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> <tbody> @foreach (var р in Model) {
174 Часть 1. Введение в инфраструктуру ASP.NET Core MVC <t r> < td>@p .N ame</td> <td >@($ " {p .Price :C2} " )</td> </t r> } </tbody> </tаЫе> <div class="text-center"> <а class="Ьtn Ьtn-primary" asp-action="AddProduct"> Add New Product </а> </div> </body> </ html> Запустив пример приложения, вы увидите по-новому стилизованное содержимое и кнопку Add New Product (Добавить новый товар) , щелчок на которой приводит к открытию формы для ввода данных. Отправка формы добавит в хранилище новый объект Produ c t и п е р енаправит браузер для отображения первоначального представ­ ления приложения (рис. 7 .1). Рис. 7 .1. Выполнение примера приложения Совет. Не забывайте, что в этом примере объекты хранятся только в памяти, т.е . любые со­ здаваемые товары при перезапуске приложения будут утеряны. Модульное тестирова н ие приложений MVC Модульные тесты используются для проверки поведения отдельных компонентов и функциональных средств в приложении, а инфраструктуры ASP.NET Core и ASP.NET Core MVC спроектированы так, чтобы максимально облегчить настройку и запуск модульных тестов для веб-приложений. В последующих разделах объясняется, как настроить модульное тестирование в Visual Studio, и демонстрируется написани е модульных тестов для приложений МVС. Кроме того, будут представлены пол езные инструменты , которые делают модульное тестирование проще и надежн ее .
Глава 7. Модульное тестирование приложений MVC 175 Доступен целый ряд разных пакетов для модульного тестирования. В книге при­ меня ется один из них, который называется xUnit.net; он выбран из-за его хорошей интеграции с Visual Studio, к тому же этот пакет использовался командой разработ­ чиков Microsoft при написании модульных тестов для ASP.NET. В табл. 7.2 приведена сводка, позволяющая поместить xUnit.net в контекст. Таблица 7.2. Помещение xUnit.net в контекст Вопрос Что это такое? Чем она полезна? Как она используется? Существуют ли какие-то скры­ тые ловушки или ограничения? Существуют ли альтернативы? Изменилась ли она по сравнению с версией MVC 5? Ответ xUnit.net - это инфраструктура модульного тестирования, которая мо­ жет применяться для тестирования приложений ASP.NET Core MVC xUпit.пet является хорошо написанной инфраструктурой модульного тестирования, которая легко интегрируется со средой Visual Studio Тесты определяются как методы, аннотированные с помощью атрибута Fa c t или Theo r y. Внутри тела метода используются методы класса Assert для сравнения ожидаемого результата теста с тем, что получи­ лось в действительности Главный просчет при модульном тестировании связан с недостаточной изоляцией тестируемого компонента . За дополнительными сведениями обращайтесь в раздел "Изолирование компонентов для модульного тестирования" далее в главе. Самой крупной проблемой, специфичной для xUпit.пet, является нехватка документации. Кое-какая базовая ин­ формация доступна на веб-сайте http: //xunit . gi thuЬ . i o , но рас­ ширенное применение требует метода проб и ошибок Доступно множество инфраструктур модульного тестирования. Двумя популярными альтернативами являются MSTest (производства Microsoft) и NUпit Инфраструктура ASP.NET Core MVC делает легким проведение модуль­ ного тестирования , но вовсе не требует и не навязывает его использо­ вание, а также не регламентирует применение конкретных инструмен­ тов тестирования . Вы вольны использовать любые инструменты или не выполнять тестирование вообще На заметку! В области модульного тестирования практически все является предметом пер­ сональных предпочтений и вопросом шумных разногласий . Некоторым разработчикам не нравится отделять модульные тесты от кода приложения, и они предпочитают определять тесты в том же самом проекте или даже в том же самом файле класса. Я описываю здесь распространенный подход , которому следую сам, но если он не кажется вам правильным, то вы должны поэкспериментировать с различными стилями тестирования, пока не под­ берете для себя подходящий. Создание проекта модульного тестирования Для приложения ASP.NEТ Core обычно создается отдельный проект Visual Studio, со­ держащий модульные тесты, каждый из которых определя ется как метод в классе С# . Применение отдельного проекта означает, что приложение можно развернуть, не раз­ вертывая одновременно тесты.
176 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Соглашение предусматривает назначение проекту модульного тестирования име­ ни <ИмяПриложения> . Tests и создание его в папке под названием test, находящей­ ся на том же уровне, что и папка src. Для приложения WorkingWithVisualStudio именем проекта модульного тестирования будет WorkingWithVisualStudio . Tests . На рис. 7.2 показана обычная структура проекта ASP.NET Core в Visual Studio, кото­ рый содержит модульные тесты. Решение _____. f----+ Проект WorkingWithVisualStudio Папка src WorkingWithVisualStudio - Папка test ,_______.... Проект - WorkingWithVisualStudio . Tests Рис. 7.2. Обычная структура проектов при модульном тестировании Создание такой структуры требует небольшой работы из-за особенностей распре­ деления средой Visual Studio содержимого решения по файлам на диске. Первый шаг заключается в использовании проводника файлов или окна командной строки для создания папки test внутри папки решения WorkingWithVisualStudio, чтобы она оказалась на одном уровне с существующей папкой src. Совет. В Visual Studio имеется шаблон проекта Unit Test (Модульный тест), однако он не настроен для применения с .NET Саге и не поддерживает средства, подобные файлу proj ect . j son. Щелкните правой кнопкой мыши на элементе решения WorkingWi thVisua lStudio в окне Solutioп Explorer среды Visual Studio (элемент верхнего уровня, охватываю­ щий все остальное), выберите в контекстном меню пункт Addc::>New Solution Folder (Добавить~:::> Новая папка решения) и установите имя новой папки в test . (Вы не смо­ жете добавить папку решения при выполняющемся отладчике; выберите пункт Stop Debugging (Остановить отладку) в меню Debug (Отладка) и попробуйте заново.) Щешшите правой кнопкой мыши на элементе папки test в окне Solution Explorer и выберите в контекстном меню пункт Addc::>New Project (Добавитьс::>Новый проект). Выберите шаблон Class Library (.NET Core) (Библиотека классов (.NET Core)) из катего­ рии lnstalledc::>Visual C#c::>.NET Core [Установленныес::>Visuаl C#c::>.NET Core), установите имя проекта в WorkingWi thVisualStudio. Tests, а в поле Location (Местоположени е) введите путь к папке test (рис. 7.3). Щелкните на кнопке ОК, чтобы создать проект. В результате структура проектов, отображаемая в окне Solution Explorer, будет соответствовать структуре папок проек­ тов в файловой системе. Конфигурирование проекта модульного тестирования Чтобы подготовить проект модульного тестирования, откройте файл proj ect . j son из проекта WorkingWi thVisualStudio . Tests и измените его содержимое так , как показано в листинге 7.5.
Глава 7. Модульное тестирование прило ж ений MVC 177 i t; 1NindO\ЧS 1Sii \.\/еЬ Co n1ole App lication (.NЕТ Core) ·-·· ••• Г.- - ' " . ." •i;;·l.:= LSearc~1~ Туре: Vis~ дprojectt/ Visual (# Core clas s;- 1 ~ЕТС?.~ 1@} 1 ASP.NEТ Core Web App lication (.NЕТ Co re) Visual (:< 1 Android 1 Cloud E:<tensibllity 1.1; 1 iOS 1 1 Repor1ing 1 1 Silv~rlight 1 Test i WCF 1 1 ~ Online ! м,mе 1 f. Lo<:atio,ru Рис. 7.3. Создание проекта для тестов Внимание! Будьте внимательны, чтобы внести изменения в файл p r o j ect . j son внутри прое кта модульного тестирования, а не проекта главного приложения. Листин г 7.5. Содержимое файла project. json из проекта WorkingWithVisualStudio.Tests "version": "1 .0 .0 - *", "testRunner": "xunit", " dependenc i es ": { "Microsoft.NETCore.App": "type ": "platform", "version": "1.0.0" }' "xunit": "2 .1 .О" , "dotnet-test-xunit": "2.2.0 -preview2-build1029" ), "frameworks": { "ne tcoreappl.0": "imports ": [ "dotnet5. 6", "portaЫ e -net 4 5+win8"] Эта конфигурация сообщает Visual Studio о том, что требуются три пакета. Пакет Microsoft . NETCore . Арр пр е доставля ет АРl-инт е рфейс .N E T Core. Пакет xuni t содержит инфраструктуру тестирования , а пакет dotnettest - x un i t обеспечива -
178 Часть 1. Введение в инфраструктуру ASP.NET Core MVC ет интеграцию между xUnit .net и Visual Studio. На момент написания книги пакет dot n et - test - xuni t , подд е рживаемый для приложен ий .NET Core , был доступен в виде предварительно й верси и, и при чтении этой главы вы можете обнаружить более ПОЗДНЮЮ версию . Когда вы сохраните изменения , внесенные в файл proj ect . j son , среда Visual Studio загрузит и устан овит NuGеt-пакеты xUnit.net вм есте с их зави с имостями . Вс е это мож ет потребовать определенного времени, поскольку зависимостей очень много . Процесс создания проектов модульного тестирования для приложений ASP.NET Core МVС в будущих выпусках Visual Studio, скорее всего, будет упроще н , по этому описан­ ные здесь дополнительные шаги больше не понадобятся. Добавление ссылки на проект приложения Для того чтобы появилась возможность тестирования кла ссов в приложении, не­ обходимо добавить ссылку на проект приложения в файл proj ect . j son про екта мо­ дульного тестирования (листинг 7.6). Листинг 7.6 . Добавление ссылки на проект приложения в файле project. j son проекта модульного тестирования "version": "1.0 .0-*", " t estRunner ": "xunit", "dependenci es ": { }' " Microsoft . NETCore . App ": " type ": " platform ", "version": "1 .0 .0" }' "xunit": "2.1.О", "dotnet-test-xunit ": "2 .2 .0-preview2-buildl029", "WorkingWithVisualStudio": "1 . 0.0" " framewo r ks": { " netcoreappl . 0 ": " imports ": [ "dotnet5 . 6 ", " portaЬle - net45+win8 " ] Написание и выполнение модульных тестов Теперь, когда все подготовительны е шаги завершены, мо жно приступать к написа­ нию ряда тестов. Первым делом добавьте в проект Wo r kingWi thVisualStudio . Tests файл класса по имени ProductTes ts . c s с определением, предст авленны м в листин­ ге 7.7. Это простой класс , но он содерж ит все, что требуется для начала модульного те стирования. На заметку! В методе Ca n ChangeP r oductPrice () умышленно допущена ошибка, кото­ рая будет устранена позже в данном разделе .
Глава 7. Модульное тестирование приложений MVC 179 Листинг 7.7 . Содержимое файла ProductTests. cs using WorkingWithVisualStudio.Models ; using Xunit ; namespace WorkingWithVisualStudio .Tests puЫic class ProductTests { [Fact] puЫic void CanChangeProductName() 11 Организация var р = new Product { Name = "Test", Price 11 Действие p.Name = " New Name "; 11 Утверждение Assert .Equal ("Ne w Name ", p .Name) ; [Fact] puЫic void CanChangeProductPrice() 11 Организация var р = new Product { Name = "Test", Price 11 Действие р.Price = 200М; 11 Утверждение Assert . Equal(lOOM , p.Price) ; lOOM }; lOOM }; В классе ProductTests присутствуют два модульных теста, каждый из ко­ торых проверяет определенное поведение класса модели Product из проекта WorkingWi thVisualStudio . Проект тестирования может содержать много классов, которые способны включать много модульных тестов . По соглашению имя тестового метода описывает то, что делает тест, а имя клас­ са - то, что подвергается тестированию . Это облегчает структурирование тестов в проекте и упрощает понимание того, какими будут результаты всех тестов, когда они выполняются средой Visual Studio. Имя ProductTests у1шзывает, что класс содер­ жит тесты для класса Product, а имена методов говорят о том , что они проверяют возможность изменения названия и цены объекта Product. Атрибут Fact применяется к каждому методу, указывая на то , что метод является: тестом. Внутри тела метода модульный тест следует шаблону , который называется орган.изация/действие/утвержден.ие (arrange/act/assert -А/А/А). Организация от­ носится: к настройке условий для: теста , действие - к выполнению теста, а утверж­ дение - к пров е рк е того, что результат оказался тем, который ожидали. Разделы организации и действия этих тестов представляют собой обычный код С#, но раздел утверждения обрабатывается: инфраструктурой xUnit.net, которая пре ­ доставляет класс по имени Assert , чьи методы используются для проверки , является ли результат действия тем, что ожидался. Совет. Атрибут Fact и класс Asset определены в пространстве имен Xuni t , для которого дол жен быть предусмотрен оператор using в каждом тестовом классе.
180 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Методы класса Assert определены как статичес1ше и применяются для выполн е­ ния разных видов сравнений меж,ду ожидаемыми и действительными р е зу л ьт атами . В табл. 7.3 описаны распространенные методы класса Assert . Таблица 7.3. Часто используемые методы класса Assert из инфраструктуры xUnit.net Имя Equal(expected , result) NotEqual(expected , result) True(result) False(result) IsType(expected, result) IsNotType(expected, resu l t) IsNull (result) IsNotNull(result) InRange(result, low , high) NotinRange(result, low ,high ) Throws(exception,expression) Описание Этот метод добавляет утверждение о том , что резуль­ тат равен ожидаемому исходу. Существуют перегру­ женные версии данного метода для сравнения различ­ ных типов и для сравнения коллекций. Имеется также версия, которая принимает дополнительный аргумент в форме объекта, который реализует интерфейс IEqua lityCornparer<T> для сравнения объектов Этот метод добавляет утверждение о том, что резуль­ тат не равен ожидаемому исх оду Этот метод добавляет утверждение о том, что резуль­ тат равен true Этот метод добавляет утверждение о том, что резуль­ тат равен false Этот метод добавляет утверждение о том, что резуль­ тат принадлежит указанному типу Этот метод добавляет утверждение о том, что резуль­ тат не принадле жит указанному типу Этот метод добавляет утверждение о том , что резуль­ тат равен null Этот метод добавляет утвер ждение о том , что резуль­ тат не равен null Этот метод добавляет утверждение о том , что резуль­ тат находится между l ow и high Этот метод добавляет утверждение о том, что резуль­ тат не находится между l ow и high Этот метод добавляет утверждение о том, что указан­ ное выражение генерирует исключение заданного типа Каждый метод класса Assert позволяет выполнять разные типы сравнения и генерирует исключение, если результат оказыва ется не тем, 1юторый ожидался. Исключение применяется для указания на то, что тест не прош ел. В тестах из лис­ тинга 7.7 метод Equal () используется для определения, корректно ли изменилось значение свойства: Assert .Equal ( " New Name " , p.Name) ; Прогон тестов с помощью окна Test Explorer Среда VisuaJ Studio предлагает поддержку для поиска и прогона мо,дульных тестов посредством окна Test Explorer (Проводник тестов), которое доступно через пункт меню TestqWiпdowsqTest Explorer (ТестqОкнаqПроводник тестов) и показано на рис. 7.4 .
Глава 7. Модульное тестирование прило ж ений MVC 181 Test Explorer Searcn Test Explorer П: ... а· Sea.:_:h Ruo А!! 1 Run". • 1 Pi•y11>t: All Test.< • ... Fai led Tests (1) Q T!!sts.Produc tTtsts .CгnCh.!11gePtoductPrict ... Passed Tests (1) (J Tests.ProductТest.<.CanC l1ongoProductNome Sun1mary Last Te:st Ru n Fail ed (Тotel Run Timl! О 00:04) О 1 Ttst Foiled О 1 Test Po"<d Рис. 7.4. Окно Test Explorer в Visual Studio • !:!х Р· l lms 3ms Совет. Если вы не видите модульные тесты в окне Test Explorer, тогда постройте решение. Компиляция запускает процесс, с помощью которого обнаруживаются модульные тесты. Выполните тесты, щелкнув на пункте Run All (Выполнить все) в окне Test Explorer. Ср еда Visual Studio прим енит xUnit.net для прогона тестов в проекте и отобразит ре ­ зул ьтаты . Как отм ечалось, тест CanChangeProductPr i ce () содержит ошибку. кото­ рая приводит к тому , что тест не проходит. Проблема связан а с аргументом метода Assert . Equa l () , из-за чего прои сходит сравнение с исходным значением свойс ­ тва Price, а не со значением , на которое оно изменилось. В листинге 7.8 проблема устранена. Совет. Когда тест не проходит, всегда полезно проверить правильность самого теста, прежде чем просматривать компонент, на который он нацелен, особенно если тест только что на­ писан или недавно модифицировался. Листинг 7.8 . Исправление теста в файле ProductTests. cs using WorkingWithVis u alStud i o . Mode l s ; using Xunit ; namespace Work i ngWithVisualStudio .Tests puЬlic class Produc t Tests { [Fact] puЫic void CanChangeProductName() 11 Организация var р = new Product {Name = "Test", Price 11 Действие p.Name = "New Name"; 11 Утверждение Assert . Equal( " New Name ", p . Name) ; lOOM };
182 Часть 1. Введение в инфраструк туру ASP.NET Соге MVC [Fac t] puЫic void CanChangeProductPrice () 11 Организация var р = new Product { Name = "Test", Price lOOM }; 11 Действие p .Price = 200 М; 11 Утверждение Assert.Equal(200M, p.Price); При наличии большого количества тестов выполнение их всех может занять неко­ торое время. Таким образом, чтобы можно было работать быстро и итеративно, в окне Test Explorer предлагаются различные варианты для выбора подмножества тестов, подлежащих прогону. Самым полезным подмножеством является набор тестов , кото­ рые не прошли (рис. 7.5). Запустите исправленный тест снова, и в окн е Test Explorer будет показано , что не прошедшие тесты отсутствуют. Test Explo re r ' _, . ~.- . (f: '"'1 ~Jt!-~~~~ Run All 1 Ruo". ? 1 Playl«t: All Te.t' ? г--~·---~~·----- . J. Failed Т( Run FAite d Tt<Sts ~ О Testsj Run Not Run Теsн " Р•s.М \ Run P•ssed Test• & Te~ts{ Repti.11L.ist Run Ctrl...R. l Su mm ary l a5t Test Run Fai led otJ I Run iin~e 0:00:04) f.З; 1 Test Fi!ifed $ 1 TostPaшd 11"Р Tosts (2) (j Tests.ProductTe-s1s.(.anChan9ePr~ductPrice: St1mma ry l ast T~t Ru n Passe:d (Tot.al Run Time 0:00:02) О 1 TestPaщd Рис . 7.5. Избирательный прогон тестов И золирование компонентов для модульного тестирова н и я Sms Написание модульных тестов для классов моделей вроде Prod uct особой сложнос­ тью не отличается. Класс Produ c t не только прост, он также самодостаточен , т.е . при вьmолнении какого-то действия над объектом P rodu c t можно иметь ув е ренность в том, что тестируется функциональность, предоставляемая классом Product . С другими компонентами в приложении МVС ситуация сложнее , потому что между ними есть зависимости. Следующий набор опр еделяемых тестов будет оп ерировать на контроллере, исследуя последовательность объектов P r odu c t, которая пер едается между контроллером и представлением .
Глава 7. Модульное тестирование прило жений MVC 183 При сравнении объектов, являющихся экземплярами специальных классов, пона­ добится использовать метод Assert. Equal () из xUnit.net, который принимает ар­ гумент, реализующий интерфейс IEquali tyComparer<T>, так что объекты можно сравнивать. Сначала необходимо добавить в проект модульного тестирования файл класса по имени Comparer . cs и поместить в него определения вспомогательных классов, приведенные в листинге 7.9. Листинг 7.9 . Содержимое файла Comparer. cs из проекта WorkingWithVisualStudio.Tests using System ; using System . Co llections.Generic ; namespace WorkingWithVisualStudio . Tests puЫic class Comparer { puЫic static Compa rer<U > Get<U>(Func<U, U , bool> func) { return new Comparer<U>(func) ; puЬlic class Comparer<T> : Comparer, IEqualityComparer<T> private Func<T , Т , bool> comparisonFunction; puЫic Comparer(Func<T , Т , bool> func) { comparisonFunction = func; puЫic bool Equa l s(T х , Ту) return comparisonFunction(x, у); puЫic int GetHashCode(T obj) return obj . GetHashCode() ; Показанные классы позволят создавать объекты IEquali tyComparer<T> с при­ менением лямбда - выражений вместо определения нового класса для каждого типа сравнения, которое нужно предпринимать . Это не является жизненно необходимым, но упростит код в классах модульных тестов и сделает его легче для понимания и сопровождения. Теперь, когда можно легко делать сравнения, давайте рассмотрим проблему зави­ симостей между компонентами в приложении. Добавим в проект WorkingWithVisualStudio. Tests новый файл класса по имени HomeControllerTests . cs и поместим в него определение модульного теста, пред­ ставленное в листинге 7.10. Листинг 7.10. Содержимое файла HomeControllerTests. cs из проекта WorkingWithVisualStudio.Tests using Microsoft . AspNetCore . Mvc; using System.Collections . Generic; using WorkingWithVisualStudio.Controllers; using WorkingWithVisualStudio . Models ; using Xunit;
184 Часть 1. Введение в инфраструктуру ASP.NET Core MVC narnespace WorkingWithVis ua lStudio.Tests puЫic class HomeControllerTests { [Fact] puЫic void IndexActionModelisCornplete() 11 Организация var control ler = new HorneController(); 11 Действие var rnode l = (controller.Index() as ViewResult)? . ViewData.Model as IE nurneraЫe <Pro duct > ; 11 Утверждение Assert . Equa l (S irn pleRepository . SharedRepos itory . Products , rnodel , Cornparer . Get<Prod uct>( (pl, р2) => pl.Narne == p2 .N arne && pl. Price == р2. Price)) ; Модульный тест в листинге 7.10 проверяет, что метод действия Index () переда­ ет представлению все объекты в хранилище. (Пока что не обращайте внимания на раздел действия: класс ViewResul t и роль, которую он играет в приложениях MVC, будут объясняться в главе 17. В настоящий момент достаточно знать. что здесь полу­ чаются данные модели, возвращаемые методом действия Index () .) Запустив тест, вы увидите, что он не проходит, указывая на отличие между набо­ ром объектов в хранилище и набором объектов, 1шторые возвратил метод Index () . Но когда дело доходит до выявления причины, из-за чего тест не прошел, возникает проблема: предполагается, что тест действует на контроллере Home , однако класс кон­ троллера зависит от класса SimpleRepository. Это затрудняет выяснение. отра зил ли тест проблему с 1шассом, на 1\Оторый он нацелен, или же проблему в другой части приложения. Пример приложения достаточно прост, чтобы можно было легко выявить пробле­ му, всего лишь взглянув на код классов HomeCont r o lle r и Simp leRepository. В ре­ альном приложении визуальный осмотр не настолько прост, т.к. цепочка зависимос­ тей способна затруднить понимание того, что привело к отказу в прохождении теста. Обычно хранилище будет полагаться на какую-то разновидность системы постоян­ ного хранения. подобную базе данных, а также библиотеку, которая предоставляет к ней доступ. Модульный тест может взаимодействовать со всей цепочкой сложных компонентов, любой из которых может вызвать проблему. Модульные тесты эффективны, когда они нацелены на небольшие части приложе­ ния, такие как отдельный метод или 1шасс. Нам необходима возможность изоляции контроллера Home от остальной части приложения, чтобы можно было ограничить область действия теста и исключить влияние со стороны хранилища . Изолирование компонента Ключом к изолированию компонентов является использование интерфейсов С# . Чтобы отделить контроллер от хранилища, добавьте в папку Mode l s новый файл класса по имени IReposi tory . cs с определением интерфейса, показанным в лис­ тинге 7.1 1.
Глава 7. Модульное тестирование приложений MVC 185 Листинг 7.11 . Содержимое файла IReposi tory. cs из папки Models us i ng System . Collect ions . Generi c ; namespace WorkingWithVisualStudio.Models puЬlic i nterface IRe pository { IEnurneraЫe<Product> Products { get ; } void AddProduct(Product р); С этим интерфейсом не связано ничего примечательного (за исключением того. что в нем не определен полный набор операций, который обычно будет нужен в веб-при­ ложении; более реалистичный и завершенный пример можно найти в главе 8). Тем не менее, добавление интерфейса вроде IReposi t ory позволяет легко изолировать ком­ понент для тестирования. Первым делом нужно обновить класс SimpleReposi tory, чтобы он реали зовывал новый интерфейс (листинг 7.12) . Листинг 7.12 . Реализация интерфейса в файле SimpleReposi tory. cs using Sys tern.Co lle ctions . Gen e ric ; narnespace WorkingWithVisualStudio . Model s puЫic class SimpleRepository : IRepository private static SirnpleRepository sharedRepository new SirnpleRepository(); privat e Dictionary<s tring , Product> products = new Dictionary<stri ng , Product>() ; puЬlic static SirnpleRepository SharedRepository = > sharedRepository ; puЫ i c SirnpleRepository() { var in itialiterns = new(J { }; new Prod uct { Name " Kayak ", Price = 275М }, new Product { Narne " Li fejacket ", Price 48 . 95М }, new Product { Narne " Soccer ball", Price 19.SOM }, new Product { Narne "C orner flag", Price 34 . 95М } foreach (var р i n i nitiali tern s) { AddProduct(p) ; products . Add( " Error ", null); puЫic IEnumeraЫe<Product> Products => products.Values; puЫic void Add Pr oduct(Product р) => pr oducts . Add(p.Name, р); Следующий шаг предусматривает модификацию контроллера, чтобы свойство, применяемое для ссылки на хранилище, использовало интерфейс, а не класс (лис­ тинг 7.13).
186 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Совет. Инфраструктура ASP.NET Core MVC поддерживает более элегантный подход к реше­ нию этой проблемы, который называется внедрением зависимостей и описан в главе 18. Внедрение зависимостей зачастую вызывает путаницу, поэтому в настоящей главе ком­ поненты изолируются более простым и ручным способом. Листинг 7.13. Добавление свойства Reposi tory в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using WorkingWithVisualStudio . Mode l s; using System .L inq ; namespace WorkingWithVisualStudio . Controllers puЬlic class HomeController : Controller ( puЫic IRepository Repository = SirnpleRepository.SharedRepository; puЫic IActionResult Index() = > View(Repository . Products . Where(р => р?.Price <50)); [HttpGet] puЫic IActionResult AddProduct () => View () ; [HttpPost] puЬlic IActionResult AddProduct(Product р) ( Reposito r y . AddProduct(p) ; return RedirectToAction("Index"); Это может выглядеть незначител ьной модификацией, но позволяет изменять хра­ нилище , которое контроллер применяет во время тестирования, что демонстри рует способ изоляции контроллера. В листинге 7 . 14 модульные тесты контроллера обнов­ лены. так что они используют специальную версию хранилища. Листинг 7 .14 . Изоляция контроллера в модульном тесте внутри файла HomeControllerTests.cs using Microsoft . AspNetCore.Mvc ; using System . Co l lections . Generic ; using WorkingWithVisualStudio . Controllers ; using WorkingWithVisualStudio . Models; using Xunit ; namespace WorkingWithVisualStudio .Tests puЫic class HomeControllerTests ( class ModelCornpleteFakeRepository : IRepository puЫic IEnurneraЫe<Product> Products { get; } = new Product [] { new Product { Narne = "Pl" , Price = 27SM }, new Product { Narne = "Р2", Price 48. 95М } , new Product { Narne "РЗ", Price = 19 . SOM}, new Product { Narne = "Р3", Price 34 . 95М } };
Глава 7. Модульное тестирование приложений MVC 187 puЫic void AddProduct (Product р) { / / ничего не делать - для теста не требуется [Fact] puЫic void IndexAct i onMode l isComple te ( ) 1 1 Организация var controll er = new HomeControll er () ; controller.Repository = new ModelCompleteFakeRepository(); 11 Действие var model = (controller . Index () as ViewResult) ?.ViewData .Model as IE nu meraЫe<Pro du ct> ; 11 Утверждение Assert.Equal(controller.Repository.Products, model, Comparer. Get<Product> ((pl, р2) => pl.Name == р2 .Name && pl.Price == p2 . Pri ce)) ; Мы определили фиктивную реализацию интерфейса IRe p osit ory, в которой при­ сутствует только свойство, необходимое для теста, и применяются всегда согласован­ ные данные (чего может не быть при работе с реальной базой данных, особенно в случа е е е совместного использования с другими разработчиками, вносящими собс­ твенные изменения) . Исправл енный модульный тест по-прежнему не проходит, указывая на то, что при­ чиной проблемы явля ется метод действия Index ( ) в классе Hom e Co n tro ller , а не компоненты , от которых он зависит. Метод действия, вызываемый модульным тес­ том, в достаточной степени прост для того, чтобы проблема стала очевидной в ре­ зул ьтате его осмотра: puЫic IActionRes ult I ndex () => View(Reposi tory .Products. Where(p => p . Pri ce < 50)) ; Проблема связана с применением м етода Wh e r e () из LINQ , который исполь зуется для фильтрации объектов Product со значениями свойства Pri ce , равным 50 или больше . Здесь уже есть серьезное указание на причину проблемы, но передовой опыт предполагает создание теста, который подтвердит наличие проблемы, прежде чем вносить корректирующую правку (листинг 7.15) . Совет. В показанных тестах присутствует много дублированного кода. В следующем разделе объясняется, как упростить тесты. Листинг 7.15. Добавление теста в файле HomeControllerTests. cs usi ng Microsoft.AspN etCore .Mvc; using System . Co ll ections . Generi c ; using WorkingWit hVisualStudi o . Co n troll e r s ; using WorkingWithVisualS t udio . Mod el s ; using Xunit ;
188 Часть 1. Введение в инфраструктуру ASP.NET Core MVC namespace WorkingWithVisualStudio. Te sts { puЫic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЫic IEnumeraЫe<Product> Products { get ; } new Product { Name " Pl '', Price 275М }, new Product { Name "Р2", Price 48.95М }' new Product { Name "РЗ", Price 19.SOM } 1 new Product {Name "РЗ11, Price 34.95М } }; puЬlic void AddProduct(Product р) { //ничего не делать - для теста не требуется [Fact] puЫic void IndexActionModelisComplete() 11 Организация = new Product[] { var controller = new HomeController() ; controller . Repository = new ModelCompleteFakeRepository() ; 11 Действие var model = (controller.Index() as ViewResult)? . ViewData.Model as IEnumeraЫe<Product> ; //Утверждение Assert .Equal(controller .Repo sitory.Products , model , Comparer.Get<Product>( (pl, р2) => pl . Name == p2.Name && pl.Price == p2 . Price)) ; class ModelCompleteFakeRepositoryPricesUnderSO puЬlic IEnumeraЫe<Product> Products { get; } new Product { Name = 11 Pl 11 , Price SM }, new Product { Name 11Р211, Price 4В.95М}, new Product { Name = 11 РЗ 11 1 Price = 19.SOM }, new Product { Name 11РЗ11, Price 34.95М } }; puЬlic void AddProduct(Product р) //ничего не делать - для теста не требуется [Fact] IReposi tory { new Product [ ] puЬlic void IndexActionМodelisCompletePricesUnderSO() / / Организация var controller = new HomeController(); controller.Repository = newModelCompleteFakeRepositoryPricesUnderSO(); //Действие var model = (controller.Index() as ViewResult)?.ViewData.Model as IEnumeraЫe<Product>; //Утверждение Assert.Equal(controller.Repository.Products, model, Comparer. Get<Product> ( (pl, р2) => pl. Name == р2 . Name && pl. Price == р2. Price) ) ;
Глава 7. Модульное тестирование прило ж ений MVC 189 Мы определили фиктивное хранилище , которое содержит только объекты Product со значениями свойства Price меньше 50, и применили его в новом тесте. Запустив этот тест, вы увидите, что он проходит, подкрепляя идею о том , что пробл ема связана с применением метода Where () в методе действия Index () . В реальном проекте выяснение причины отказа теста означает необходимость со­ гласования цели теста со спецификацией для прилож ения. Вполне может оказаться, что метод Index () обязан фильтровать объекты Product по свойству Price , в случае чего тест нуждается в пересмотре. Это распространенный итог, и тест, который не прошел, вовсе не всегда указывает на присутствие реальной проблемы в приложении. С другой стороны, если метод действия Index () не должен фильтровать объекты мо­ дели, тогда потребуется внести корректирующую правку (листинг 7.16). Понятие разработки через тестирование В этой главе я придерживаюсь наиболее часто используемого стиля модульного тестиро­ вания, при котором мы пишем функцию приложения и затем тестируем ее, чтобы удосто­ вериться в том, что она работает требуемым образом. Такой стиль популярен, поскольку большинство разработчиков думают сначала о прикладном коде, а затем о его тестирова­ нии (я определенно подпадаю в эту категорию). Проблема такого подхода в том, что он склоняет к созданию модульных тестов только для того прикладного кода, который было трудно писать или серьезно отлаживать , оставляя ряд аспектов каких-то функций лишь частично протестированными или вообще без тестирования . Альтернативным подходом является разработка через тестирование (Test-Driveп Developmeпt - TDD) . Существует много вариаций TDD, но основная идея в том, что тесты для функции пишутся до того, как она реализуется. Написание тестов первыми заставля ­ ет более тщательно думать о реализуемой спецификации и о том, как узнать, что функция была реализована корректно . Вместо погружения в детали реализации подход TDD застав­ ляет заранее продумывать, чем будет измеряться успех или неудача. Все создаваемые тесты изначально не будут проходить, т.к. новая функция еще не реали­ зована . Однако по мере добавления кода в приложение тесты постепенно будут переходить из красного состояния в зеленое , и ко времени завершения функции все тесты окажутся пройденными. Подход TDD требует дисциплины, но приводит к получению более полного набора тестов и может дать в результате более надежный и устойчивый код. Листинг 7. 16. Удаление фильтрации посредством LINQ в файле HomeController. cs using Microsoft . AspNe tC ore.Mvc; using WorkingWithVisualStudio . Models ; using System . Linq ; namespace WorkingWithVisualStudio . Controllers puЫic class HomeController : Controller { puЫic IRepository Repository = SimpleRepository .SharedRepository ; puЬlic IActionResul t Index () => View (Repository. Products) ; [HttpGet] puЫic IActionResult AddProduct() = > View( new Product() ) ; [HttpPostJ puЫic IActionResult AddProduct(Product р) { Repository . AddProduct(p) ; return RedirectToAction( " Index ");
190 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Запустив тесты снова, вы увидите, что все они проходят (рис . 7 .6). Run All 1 Run". • Playl ist : All Tests • А Passed Tests (4) О Tests.HomeControllerTests.lnd~xActio nModell sComplete О Tests .H omeControllerTe.sts.l r1de xAction Mod ell sComp lete.. . () Tests .ProductTests.CanChangeProductNome О Tests .ProductTests .Ca nCha ngeP ro duct Price Summary l ast Test Run Passed (Тo tal Run Time 0:00 :04) О 4 Tesls Passed Рис. 7.6. Прохождение всех тестов Р· 1ms 36 ms 1ms 9ms Может показаться, что мы проделали слишком много работы для устранения та­ кой простой проблемы, однако возможность протестировать отдельный компонент жизненно важна при построении реального приложения. Достижение точки, где вы идентифицировали проблему и написали тесты для проверки исправления, возможна только тогда, когда вы способны эффективно изолировать компоненты. Улучшение модульных тестов В предыдущем разделе был представлен базовый подход к написанию модульных тестов и прогону тестов в Visual Studio, а также акцентировано внимание на важ­ ности изоляции тестируемого компонента. В этом разделе рассматриваются расши­ ренные инструменты и средства, которые можно применять для написания тестов более согласованным и выразительным образом. Если вы сильно увлечетесь модуль­ ным тестированием, то можете в итоге получить настолько много тестового кода, что его ясность становится очень важной, особенно с учетом необходимости пересмотра тестов для отражения изменений в приложении, вносимых во время разработки и сопровождения. Параметризация модульного теста Тесты, написанные для класса Ho meC o n trol l er, выявили проблему, которая присутствовала только для определенных значений данных. Чтобы проверить это ус­ ловие , были созданы два похожих теста, каждый из которых содержал собственное фиктивное хранилище. При таком подходе появляется дублированный код. особен­ но учитывая то, что единственное отличие между двумя тестами связано с набором значений decimal, которые указываются для свойства Price объ ектов Product в фиктивных хранилищах.
Глава 7. Модульное тестирование nриложений MVC 191 Инфраструктура xUnit.net предлагает поддержку параметризованных тестов, когда данные, используемые в тесте, из него удаляются, так что единственный ме­ тод может применяться для множества тестов. В листинге 7.17 с помощью средс­ тва параметризованных тестов устраняется дублированный код в тестах для класса HorneController. Листинг 7.17 . Параметризация модульного теста в файле HorneControllerTests. cs using Microsoft . AspNetCore . Mvc ; using System.Collections .Gene ric; using WorkingWithVisualStudio.Controllers ; using WorkingWithVisualStudio . Models; using Xunit ; namespace WorkingWithVisualStudio . Tests puЫic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЫic IEnumeraЫe<Product> Products { get; set; puЫic void AddProduct(Product р) { 11 ничего не делать - для теста не требуется [Theory] [InlineData(275, 48.95, 19.50, 24.95)] [InlineData(5, 48.95, 19.50, 24.95)] puЫic void IndexActionМodelisComplete(decimal pricel, decimal price2, decimal priceЗ, decimal price4) 11 Организация var controller = new HomeController(); controller.Repository = new ModelCompleteFakeRepository }; Products = new Product [] { new Product {Name "Pl", Price new Product {Name "Р2", Price new Product {Name new Product {Narne 11РЗ11, Price = "Р4", Price 11 Действие = pricel }, = price2 }, = priceЗ }, = price4 }, var mod el = (control ler.Index() as ViewResult)? . ViewData.Model as IEnurneraЫe<Product> ; 11 Утверждение Assert . Equal(controller . Repository .Products , model , Comparer .Get <Product>( (pl, р2) => pl.Name == p2.Name && pl . Price == р2 . Price)); Параметризованные модульные тесты помечаются атрибутом Theory вместо ат­ рибута Fact, который используется для стандартных тестов. Здесь таюке применя­ ется атрибут InlineData, который позволяет указывать значения для аргументов,
192 Часть 1. Введение в инфраструктуру ASP.NET Core MVC определяемых методом модульного теста. Язык С# ограничивает способ выражения значений данных в атрибутах, поэтому мы определили четыре аргумента decimal тестового метода и посредством атрибута InlineData предоставили для них значе­ ния. Значения decimal внутри тестового метода используются для генерации масси­ ва объектов Product, который применяется для установки свойства Products объ­ екта фиктивного хранилища. Каждый атрибут Inline определяет отдельный модульный тест, который пока­ зан как индивидуальный элемент в окне Test Explorer среды Visual Studio (рис . 7.7) . Запись в окне Test Explorer отображает значения. которые будут использоваться для аргументов метода модульного теста. T~t Expl ortr • 1:) х Р· PJJnAU 1 !Wn.• • 1 Playlis t : All Te;ts • А Passed Tбis (4) Summa ry last Tes:t Run Passed (Тot31 Run Т t) 4 Tests Ptssed О Tests.Pre>Cluct e-sts .Ca nCh.a1'9eProductNome $ Tests.Prod1.1ctT~tsC~ Cl'\an.g~ProductPricl!! 2 rns 8ms Рис. 7.7 . Параметризованные тесты в окне Test Explorer среды Visual Studio Получение тестовых данных из метода или свойства Ограничения, налагаемые на выражение данных в атрибутах, сокращает полез­ ность атрибута I n 1 i n е Da t а. Альтернативный подход предусматривает создание статического метода или свойства, возвращающего объект, который требуется для тестирования. В такой ситуации нет никаких ограничений относительно того, как определяются данные, и можно создавать более широкий диапазон тестовых значе­ ний. Чтобы продемонстрировать подход в работе, добавим в проект модульного тес­ тирования файл класса по имени ProductTestData . cs с определением , показанным в листинге 7.18. Листинг 7.18. Содержимое файла ProductTestData. cs из проекта WorkingWithVisualStudio.Tests using System . Collections; using System.Collections.Generic; using WorkingWithVisualStudio . Models; namespace WorkingWithVisualStudio.Tests puЫic class Produ c tTestData : IEnumeraЫe<object[]> puЫic IEnumerator<object[]> GetEnumerator() { yield return new object[] { GetPricesUnder50() }; yield return new object[] { GetPricesOverSO };
Глава 7. Модульное тестирование прило жений MVC 193 IEnumerator IEnumeraЫe . GetEnumerator() return this.GetEnumerator() ; private IEnumeraЫe<Product> GetPricesUnder50() decimal[] prices = new decimal[] { 275, 49 . 95М , 19.SOM, 24 . 95М } ; for (int i =О; i <prices.Length; i++) { yield return new Product { Name = $"P{i + 1}", Price = prices[i] }; private Product [ ] GetPricesOver50 => new Product[ ] new Product { Name "Pl" , Price 5 }, new Product { Name "Р2", Price 48 . 95М }, new Product { Name "РЗ", Price 19.SOM }, new Product { Name "Р4", Price 24 . 95М } }; Тестовые данные пр едоставляются через класс, который реализует интерфейс IEnumeraЫe < obj ect [] >, возвращающий последовательность массивов объектов. Каждый массив объектов в посл едовательн ости содержит один набор аргументов, которые будут передаваться тестовому методу. Мы пе реопределим тестовый метод, чтобы он принимал массив объектов Product, который добавит к тестовым данным еще один уровень - перечисление массивов объектов Product. Так ая глубина струк­ туры тестовых данных может сбивать с толку, но важно понимать, что инач е т есты не будут работать, когда количество аргументов, которые xUпit.net пытается пе редать тестовому методу, не соответствует сигнатуре метода. Мне нравится имеющаяся структура классов тестовых данных, когда закрытые методы или свойства определяют индивидуальные наборы тестовых данных, кото­ рые затем с помощью метода GetEnumerator () объединяются в последовател ьности массивов объектов. Для иллюстрации различных прием ов массивы объектов Product были созданы с применением и метода, и свойства, но я пр едпочитаю использовать в своих про ектах один подход (на его выбор влия ет вид данных, с ноторыми проводится тестирование). В листинге 7.19 показано, как можно прим енять класс тестовых дан­ ных с ат рибутом Theory для настройки тестов. Совет. Если вы хотите включить тестовые данные в тот же самый класс, который определяет модульные тесты, тогда можете использовать атрибут MemberData вместо ClassData. Атрибут MemЬerData конфигурируется с применением строки, указывающей имя стати­ ческого метода, который будет предоставлять реализацию IEnumeraЫe<object [ ] >, где каждый массив объектов в последовательности является набором аргументов для тес­ тового метода. Листинг 7.19. Использование класса тестовых данных в файле HomeControllerTests.cs using Microsoft . AspNetCore . Mvc ; using System . Collections . Generic ; using WorkingWithVisualStudio . Controllers ; using WorkingWithVisualStudio . Models; using Xunit ;
194 Ча сть 1. Введение в инфраструктуру ASP.NET Соге MVC namespace WorkingWithVisualStudio . Tests { puЬlic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЫic IEnumeraЫe<Product> Products { get; set; puЫic void AddProduct(Product р) { 11 ничего не делать - для теста не требуется [Theory] [ClassData(typeof(ProductTestData))] puЫic void IndexActionМodelisComplete (Product [] products) { 11 Организация var controller = new HomeController(); controller . Repository = new ModelCompleteFakeRepository Products = products }; 11 Действие var model = (contro ller . Index() as ViewResu l t)? . ViewData . Model as IEnumeraЫe<Product> ; 11 Утверждение Assert.Equal(controller . Repository . Products , model , Comparer . Get<Product>( (pl , р2) => pl . Name = = p2 . Name && pl . Price = = p2.Price)) ; Атрибут ClassData конфигурируется с типом класса тестовых данных, которым в рассматриваемом случае является ProductTestData. Во время выполнения тестов ин­ фраструктура xUnit.net создаст новый экземпляр класса ProductTestData, после чего будет применять его для получения последовательности тестовых данных для теста. На заметку! Если вы посмотрите на список тестов в окне Test Explorer, то увидите, что для тестов IndexActi onModelisComp lete предусмотрена единственная запись, хотя класс ProductTestData предоставляет два набора тестовых данны х . Так происходит в ситуации , когда объекты тестовых данных не удается сериализировать, и проблему можно решить, применив к тестовым объектам атрибут SerializaЫe. Улучшение фиктивных реализаций Эффективная изоляция компонентов требует фиктивных реализаций классов, что­ бы предоставить тестовые данные или проверить, ведет ли себя компонент так, как должен. В предшествующих примерах создавался класс, реализующий интерфейс IReposi tory . Это мог быть эффективный подход, но он приводил к созданию клас­ сов реализаций для каждого вида теста, который требовалось запуск ать. В качестве примера в листинге 7.20 демонстрируется добавление теста, который проверяет, что метод действия Index () вызывает метод Products () в хранилище толыю один раз . (Такая разновидность теста распространена, когда имеется опасение , что компонент делает дублирующиеся запросы 1< хранилищу, становясь причиной множества запро­ сов к базе данных. )
Глава 7. Модульное тестирование прило ж ений MVC 195 Листинг 7.20. Доба вление модульного теста в файле HomeControllerTests. cs using Microsoft.AspNetCore . Mvc ; using System . Collections .Gener ic ; using WorkingWithVisualStudio . Contro llers; using WorkingWithVisualStudi o . Models ; using Xunit ; using System ; namespace WorkingWithVisualStudio . Tests puЫic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЫic IEnumera Ы e<Product> Prod ucts { get ; set ; puЫic void AddP r oduct(Product р) { 11 ничего не делать - для теста не требуется [Theory] [ClassData(typeof(ProductTestData))] puЫic void IndexActionModelisComplete(Product[J products) { 11 Организация var controller = new HomeController(); controller . Repository = new ModelCompleteFakeRepository Products = products ); 11 Действие var model = (control ler.Inde x() as Vi ewResult)? . ViewData . Model as IEnumeraЫe<Product> ; 11 Утверждение Assert . Equal(control l er . Repository . Products , model, Comparer . Get<Product>( (pl , р2) => pl.Name = = p2 . Name && pl . Price == p2 .Price)) ; class PropertyOnceFakeRepository IRepository puЫic int PropertyCounter { get; set; } = О; puЫic IEnumeraЫe<Product> Products { get { PropertyCounter++; return new[] { new Product { Name 11 Pl11 , Price puЬlic void AddProduct(Product р) / / ничего не делать - для теста не требуется [Fact] puЬlic void RepositoryPropertyCalledOnce() { / / Организация var repo = new PropertyOnce FakeRepository(); 100}}; var controller = new HomeController { Repository = repo };
196 Часть 1. Введение в инфраструктуру ASP.NET Core MVC // Действие var resul t = controller. Index () ; / / Утверждение Assert.Equal(l, repo.PropertyCounter); } Фиктивные реализации не всегда являются простыми источниками данных; они также могут использоваться для оценки способа, которым компоненты выполняют свою работу. В этом случае было добавлено простое свойство счетчика, которое уве­ личивается каждый раз, когда читается свойство Products фиктивного хранилища, и с помощью метода Assert. Equal () выполнена проверка , что обращение 1< свойс­ тву производилось только один раз. Добавление инфраструктуры имитации Создание фиктивных объектов подобного рода выходит из-под контроля, и лучший способ вернуть себе контроль - применить инфраструктуру фиктивных объектов, также называемую ин.фраструктурой имитации. (Существует формальная разница между фиктивными и имитированными объектами, но для облегчения использования современные инструменты тестирования размывают ее , поэтому данные термины бу­ дут применяться взаимозаменяемо.) В настоящей главе используется инфраструктура Moq , которая описана в табл. 7.4 . Таблица 7 .4 . Помещение Moq в контекст Вопрос Что это такое? Чем она полезна? Как она используется? Существуют ли какие-то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменилась ли она по сравнению с версией MVC 5? Ответ Moq - это программный пакет для создания фиктивных реали­ заций компонентов в приложении Инфраструктура имитации облегчает создание фиктивных ком­ понентов для изоляции частей приложения в целях модульного тестирования Moq применяет лямбда-выражения для определения функцио­ нальности фиктивных компонентов и требует определения толь­ ко тех средств, которые используются при тестировании Привыкание к синтаксису может потребовать некоторых усилий. Документация и примеры находятся по адресу https : //githuЬ.com/Moq/moq4 Доступно несколько альтернативны х инфраструктур, в том числе NSubstitute (h ttp : / /nsubstitute . githuЬ . io) и FakeltEasy (h ttp : / /fakeiteasy . github. io). Все эти инфраструктуры предлагают похожие возможности, и выбор между ними связан только с предпочитаемым вами синтаксисом Применение инфраструктуры имитации специфично для модуль­ ного тестирования и не является требованием ASP.NET Core MVC
Глава 7. Модульное тестирование приложений MVC 197 При написании этой главы платформа .NЕТ Core находилась на ранней стадии своей адаптации, и основные пакеты имитации пока еще не поддерживали ее. В Microsoft создали специальное ответвление проекта Moq и перенесли его для ра- боты с . NЕТ Core, так что именно этот пакет будет использоваться в книге . Тем не менее, поскольку это не главный выпуск Moq. требуется дополнительный шаг для конфигурирования NuGet с целью загрузки пакета Microsoft. Выберите в Visual Studio пункт меню Tools~Options (Сервис<=:>Параметры) и в от­ рывшемся диалоговом окн е перейдите в раздел NuGet Package Manager~ Package Sources (Диспетчер пакетов NuGеt<=:>Источники пакетов), как по1шзано на рис . 7.8. Здесь можно сконфигурировать источники пакетов . 1 Sear<h Optons (Ctrl • E) ~ En\rironment 1 ~ Projocts ond Solu tion< 1 t> Sошсе Control 1 ~ Тoxt Ed~or 1 t• Debugging ~ Pelfo1n11nco Тools ~ D•tabase Тools 1 1 1> Graphks Di~g nostic_'5 А NuGet Packoge M•noger . Generol ~ SQl Server Тools ~ Te>:t Temp lating 0 api.nuget.org https :/iopi .nuget .org/v3/ indexJ<on Ma chin e·wide pockog e 1 P.aclc_гge-S9u rCe:s- t> Tools for Apache Cordovв 0 Mic rosoft and . NЕТ 1 ~ Web https://v1Vм.nu9et.org/api/v2/cural<d·feeds/n1ic rosoftdotneV 1> W~b Forms Designe r J0 Microsoft Vtsual Studio Offline Packages ~ Web Performanco Тest Тools C:\Proarom Files (xSб)\Microsoft SOКs\NuGetPockoaes\ 1 1> Windows Forms Designeг 1 ~ 'Norkflow Designer !':{o mo: /nuget.org ~ XAML Oesig ntt ~ourc'"J~~~~~2_1_______l[J [Noр~ l----------------------------~~~ё;~_:~_,, Рис. 7 .8 . Конфигурирование источников пакетов NuGet Щелкните на кнопке с изображением знака "плюс" зеленого цвета и введите в по­ лях Name (Имя) и Source (Источник) детальную информацию, описанную в табл. 7.5. Таблица 7.5. Настройки, требуемые для источника пакетов NuGet Поле Name Source Значение ASP . NET Contrib https : //www . myget . org/F/aspnet - contriЬ/api/vЗ/index .j son Щелкните на кнопке Update (Обновить), чтобы задействовать значения, вв еденные в полях, и затем на Rнопке ОК для закрытия диалогового окна настроек. Совет. Применение версии Moq от Microsoft является краткосрочной мерой; вам не придется ее использовать после того, как в результате основных усилий по разработке добавится помержка .NET Core . Когда это произойдет, вы сможете следовать инструкциям по уста­ новке Moq, доступным по адресу h t tp : / / g i thub. com/moq/moq4.
198 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC В листинге 7.21 в файл proj ec t. j so n проекта модульного тестирования до­ бавляется версия Moq от Microsoft (известн ая как moq . netcore) наряду с пакетом System . Diagn o s tics . Trace S o ur ce, от которого з ависит moq . netcore . Листинг 7 .21 . Добавление Moq в файл proj ect. j son проекта WorkingWithVisualStudio.Tests " ve r sion": "1.0 .0-*", " test Runner" : "xunit ", " dependencies ": { }1 "Microsoft.NETCore .App ": "type": "pl atform", "version": "1 .0 .0" }1 "xunit": "2 .1 .0", "dotnet-test-xunit ": "2 .2 .0 -preview2-build1029", "WorkingWithVisualStudio": "1.0 .0", "rnoq.netcore": "4. 4. О-Ьеtа8", "Systern.Diagnostics . TraceSource": "4 . 0.0" " f r ameworks": { " netcoreappl . 0 ": " i mports": [ "dotnetS. 6 ", "portaЬle-net45+win8"] Создание имитированного объекта Создание имитированного объ екта озн ачает необходимость сообщения Moq о том, какого вида объ ект вас интересует, конфигурирование его поведения и примен ение этого объекта к тестируемой сущности. В листинге 7 .22 инфрастру~<тура Moq исполь­ зуется для замены двух фиктивных хранилищ в тестах для класса HomeCon t r o ller. Листинг 7.22 . Применение имитированных объектов в файле HomeControllerTests.cs using Microsoft . AspNetCore . Mvc ; using System. Collections . Generic ; using WorkingWithVi s ua l St u dio . Controllers ; u sing WorkingWithVi sualSt ud io . Mode l s ; using Xunit ; using System ; using Moq; namespace WorkingWi thVisual Studi o .Tests puЫ i c class HomeControl l e r Tests { [Theory ] [Cl assDat a(t ypeof(ProductTestDat a ) )] puЫic void Ind e x ActionModelisComplete(Prod u ct[] p r oducts) { 11 Орга н изация var rnock = new Mock<IReposi tory> () ;
Глава 7. Модульное тестирование прило же ний MVC 199 mock.SetupGet(m => m.Products) .Returns(products) ; var controller = new HomeController { Repository = mock.Object }; 11 Действие var model = (controller.Index() as ViewResult)? . ViewData . Model as IEnumeraЬle<Produc t>; 11 Утверждение Assert.Equal(controller.Repository . Products , model, Comparer . Get<Product>( (pl, р2) => pl . Name == p2 . Name && p l. Price == р2. Price)) ; [Fact) puЫic void Reposito ryPropertyCalledOnce() 11 Организация var mock = new Mock<IRepository>(); mock.SetupGet(m => m.Products) . Returns (new [] { new Product { Name = "Pl", Price = 100 } }) ; var controller = new HomeController { Repository = mock.Object }; 11 Действие var result = controller.Index(); 11 Утверждение mock.VerifyGet(m => m.Products, Times.Once); Использование Moq позволило удалить фиктивные реализации интерфейса IRepository и заменить их всего несколькими строками кода. Здесь не будут рас­ сматриваться детальные сведения о разных поддерживаемых Moq средствах. но даны объяснения способа применения Moq в примерах. Примеры и документация по Moq доступны по адресу https : //github . com/Moq/moq4 . (При объяснении модульного тестирования различных типов компонентов МVС в оставшихся главах книги также будут приводиться соответствующие примеры . ) Первым делом создается новый объект Mock с указанием интерфейса, который должен быть реализован: var mock = new Mock< IRepository> () ; Созданный объект Mock будет имитировать интерфейс IReposi tory. Далее оп­ ределяется функциональность, которая требуется для теста. В отличие от обычной реализации интерфейса классом для имитированного объекта конфигурируется толь­ ко поведение, нужное для теста. В первом имитированном хранилище понадобится реализовать свойство Products, чтобы оно возвращало набор объектов Product, ко­ торый передается тестовому методу через атрибут ClassData: mock . SetupGet(m => m. Products) .Returns(products) ; Метод SetupGet () используется для реализации средства извлечения для свойс­ тва. Аргументом этого метода является лямбда-выражение, которое указывает подле-
200 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC жащее реализации свойство (Products в данном примере). Метод Returns () вызы­ вается на результате, полученном из метода SetupGet (), чтобы указать результат, который будет возвращаться, I<огда читается значение свойства. Для второго имити­ рованного хранилища применяется тот же самый подход. но указывается фиксиро­ ванное значение: mock . SetupGet(m => m.Products) . Returns (new[] { new Product { Name = "Pl", Price = 100 } }) ; В классе Mock определено свойство Object, возвращающее объект, который ре­ ализует указанный интерфейс и обладает ранее определенным пов едением. В обоих модульных тестах свойство Obj ect используется, чтобы получить хранилище для конфигурирования контроллера: var controller = new HomeControl l er Repository = mock.Object } ; Последним примененным средством Moq была проверка того, что к свойству Products осуществлялось толыщ одно обращение: mock . VerifyGet(m => m.P roducts , Times . Once) ; Метод Veri f yGet () относится к тем методам класса Mock , которые инспекти­ руют состояние имитированного объекта, когда тест завершен. В этом случае ме­ тод VerifyGet () позволяет проверить, сколько раз читалось свойство Products . Значение Times . Once указывает, что метод VerifyGet () должен генерировать ис­ ключение, если свойство читалось не в точности один раз, и это приведет к тому, что тест не пройдет. (Методы класса Assert , обычно используемые в тестах, генерируют исключение, когда тест не проходит, и потому при работе с имитированными объ ек­ тами метод VerifyGet (} можно применять для замены метода класса Assert .) Резюме Большая часть этой главы была сконцентрирована на модульном тестировании, которое может оказаться мощным инструментом для повышения качества кода. Модульное тестирование не подойдет абсолютно каждому разработчику, но с ним по­ лезно поэксперим ентировать, и оно может быть поле з ным, даже есл и используется только для сложных функций или обнаружения проблем. Бьшо описано прим енени е инфраструктуры тестирования xUnit.net, объяснена важность изоляции компонентов для целей тестирования и продемонстрированы некоторые инструменты и при е мы, позволяющие упростить 1юд модульных тестов. В следующей главе начнется разра­ ботка более реалистичного приложения МVС, чтобы показать, как работают вместе различные функциональные компоненты , прежде чем погружаться в характерные де­ тали в части II настоящей книги .
ГЛАВА 8 SportsStore: реальное приложение в предшествующих главах мы создавали очень простое приложение МVС. Был описан паттерн MVC, основные средства языка С#, а также инструменты, не­ обходимые профессиональным разработчикам приложений MVC. Наступило время собрать все вместе и построить несложное, но реалистичное приложение электрон­ ной коммерции. Наше приложение под названием SportsStore будет следовать классическому под­ ходу, который повсеместно используется в онлайновых магазинах . Мы создадим он­ лайновый каталог товаров, который потребители могут просматривать по категори­ ям и страницам, корзину для покупок, куда пользователи могут добавлять и удалять товары, и форму оплаты, где потребители могут вводить сведения, связанные с до­ ставкой. Кроме того, мы создадим административную область, которая включает в себя средства создания, чтения, обновления и удаления (create, read, update, delete - CRUD) для управления каталогом товаров, и защитим ее так, чтобы изменения могли вносить только зарегистрированны е администраторы. Цел ь этой и последУющих глав - дать вам возможность увидеть, на что похожа реальная разработка приложений MVC, за счет создания примера приложения, кото­ рый максимально приближен к реальности. Разумеется, мы будем ориентироваться на ASP.NET Core MVC, поэтому интеграция с внешними системами, такими как база данных, предельно упрощена, а определенные части приложения, например, обработ­ ка платежей , вообще отброшены. Построение всех уровней необходимой инфраструктуры может показаться не­ сколько медленным, но первоначальные трудозатраты при разработке приложения MVC окупаются, обеспечивая удобный в сопровожде нии, расширяемый и хорошо структурированный код с великолепной поддержкой модУльного тестирования. Модульное тестирование Я уже достаточно много говорил о легкости проведения модульного тестирования в MVC, а та кже о том , что модульное тестирование м оже т быть важной и полезной частью процесса разр аботки . Это будет демонстрироваться на протя жении данной части книги, поскольку я описываю нюансы и приемы модульного тестирования в их взаимосвязи с основными средствами MVC.
202 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Я понимаю, что мое мнение не является единственно верным. Если вы не хотите прибе­ гать к модульному тестированию, то меня это вполне устроит. Таким образом, когда что­ то относится исключительно к тестированию, оно будет помещаться во врезку, подобную настоящей. Если модульное тестирование вас не интересует, то можете смело пропускать эти врезки, и приложение SportsStore будет работать не менее успешно. Чтобы восполь­ зоваться преимуществами технологии ASP.NET Саге MVC, вовсе не обязательно проводить какое-либо модульное тестирование, хотя поддержка тестирования, конечно же, является основной причиной перехода на ASP.NET Саге MVC. Большинству средств MVC, используемых в приложении SportsStore, посвящены отдельные главы далее в книге. Вместо того чтобы повторять весь материал , я сооб­ щаю ровно столько, сколько требуется для понимания этого примера приложения, и указываю главу, в которой можно почерпнуть более подробные сведения. Каждый шаг, необходимый для построения приложения, будет выделен, что поз­ волит понять, каким образом средства MVC сочетаются друг с другом. Вы должны уделить особое внимание созданию представлений . Если вы не будете в точности сле­ довать примерам, то получите несколько странные результаты. На заметку! В Microsoft заявили , что в следующей версии Visual Studio изменят инстру ­ ментарий, применяемый для создания приложений ASP.NET Core MVC. Проверяйте веб-сайт издательства на предмет обновлений, которые появятся после выпуска новых инструментов. Начало работы Если вы планируете писать код приложения SportsStore на своем компьютере во время изучения материала этой части книги, то вам придется установить Visual Studio и удостовериться в том, что установлен вариант LocalDB, который требуе тся для постоянного хранения данных. На заметку! Если вы просто хотите работать с проектом, не воссоздавая его, тогда може­ те загрузить готовый проект SportsStore как часть загружаемого кода примеров для на­ стоящей книги, который доступен на веб-сайте издательства. Разумеется, вы вовсе не обязаны повторять все действия. Я старался делать снимки экрана и листинги кода мак­ симально простыми в отслеживании на тот случай, если вы читаете эту книгу в поезде , кафе или где-то еще. Создание проекта MVC Мы будем следовать тому же самому базовому подходу, который использовал­ ся в предшествующих главах и заключается в том, чтобы начать с пустого проек ­ та и добавлять в него все необходимые конфигурационные файлы и компоненты . Выберите в меню File (Файл) среды Visual Studio пункт NewqProject (СоздатьqПроект) и укажите шаблон проекта ASP .NET Саге Web Application (.NET Саге) (Веб-приложение ASP.NET Core (.NET Core)), как показано на рис. 8.1 . В качестве имени проекта введите Spo r ts Stor e и щелкните на кнопке ОК.
Глава 8. SportsStore: реальное приложение 203 "' 1emp14tts "' Vi~ual С'* "W indo ~ i.'! •l>• . NfТ Core дndroid Cloud f!. Online Solu tion name: 4 mASP.NEТ V/eb Applicdtion (.NfТ FramбVork) 11 @ ASP.NEТ Core Wtb Applk ati on (.N ЕТ fra meowork} SportsS-tore ~c:\p_r~~ct~ ~. ,Sport sSl:o re Рис. 8. 1. Выбор типа проекта Visu.:i l (# ТУР": ; Pr oj« Corea: , ondO~ 9Appl~ 0 Ad~ Opt t uУф Выберите шаблон Empty (Пустой), как проиллюстрировано на рис. 8.2, и щелкните на кнопке ОК, чтобы создать проект Sp orts Store . S•l•d • l •mplat<: г~;;:;C:reT;,;;i" ------·-··------1 li ~m 1 j1 Web API App~::i;on j 1i 1 1 An empty project temp late for crtating "" ASP.NП Со1е 1 ~pp lic.5tfo n . This; template does not h~e any content in J ~ 1 1! 1 1i ~ г~~~~-~~~c;titJ~J 1 11_ ------ ------- ____________" ___11 Authentication: NoAuthentkatlon 1 1 1 ~ Microsoft Azu re G О Host in the cJoud 1 !,Арр Se~;, ...._. .:] 1 Рис. 8.2. Выбор шаблона проекта Добавление пакетов NuGet 1 [ ОК ']j_C..nceJ 1j --- -----· ------ Шаблон проекта Empty устанавливает базовые средства ASP.NET Core , но требу­ ются дополнительные пакеты, чтобы предоставить функциональность, обязатель­ ную для приложений МVС. В листинге 8.1 приведены изменения, внесенные в файл p r oj ect . j son, которые добавляют пакеты, необходимые для того , чтобы начать раз­ работку приложения SportsStore.
204 Часть 1. Введение в инфраструктуру ASP.NET Co re MVC Листинг 8.1 . Добавление пакетов NuGet в файле proj ect. j son " dependencies ": { " Microsoft . NETCore . App ": "version": "1 .0 .0", " type ": " platform " }' " Microsoft .AspNetCore . Diagnostics " : " 1 . О . 0 ", " Microsoft . AspNetCore . Server .I IS i ntegration ": "1. 0 . 0 ", " Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . О ", " Microsoft.Extensions . Logging . Console ": " 1.0 . 0 ", "Мicrosoft. AspNetCore. Razor. Tools": { }' "version": 11 1. О . 0-preview2-final", "type": "Ьuild" "Мicrosoft. AspNetCore. StaticFiles": "1. О. О", "Мicrosoft.AspNetCor e.Mvc": "1. О. 0" )' "tools": { "Мicrosoft.AspNetCore.Razor.Tools": "1.0 . 0 -preview2-final 11 , " Microsoft . AspNetCore . Server .I ISintegration.Tools": " 1.0 .0 -preview2-final " )' " frameworks ": { " netcoreappl . 0 ": " imports ": [ "dotnetS . 6 ", "portaЫe - net45+win8 " ] )' " buildOptions ": }' " emitEntryPoint ": true , " preserveCompilationContext ": true " runtimeOptions ": { " configProperties ": " System.GC .Server" : true }' " puЬlishOptions ": " include ": ["wwwroot ", " web .config " ] )' " scripts ": { " postpuЫish ": [ " dotnet puЫish - iis -- puЫish - folder % puЬlish : OutputPath % -- framework % puЫish : FullTargetFramework % " } Пакеты, добавленные в раздел dependencies файла proj ect . j son, предостав­ ляют самую базовую функциональность, требующуюся для начала разработки при­ ложения MVC. По мере разработки приложения SportsStore будут добавляться и дру­ гие пакеты, но пакеты в разделе dependencies являются хорошей отправной точкой (табл. 8.1).
Глава 8. SportsStore: реальное приложение 205 Кроме пакетов в разделе dependencies в листинге 8.1 показано добавление в раздел tools файла proj ect . j son, которое конфигурирует пакет Microsof t. AspNetCore . Razor . Tools для применения в Visual Studio. Он включает средство IntelliSense для встроенных дескрипторных вспомогательных классов, используемых для создания НТМL-содержимого, которое приспособлено под конфигурацию прило­ жения MVC. Таблица 8.1 . Дополнительные пакеты NuGet в файле proj ect. j son Имя Описание Microsoft . AspNetCore. Mvc Этот пакет содержит инфраструктуру ASP. NET Core MVC и предоставляет доступ к основным средс­ твам , таким как контроллеры и представления Razor Microsoft .AspNetCo re. StaticFiles Этот пакет обеспечивает поддержку для обслужи­ вания статических файлов, таких как файлы изоб­ ражений , JavaScript и CSS, из папки wwwroot Microsoft . AspNetCore. Razor . Tools Этот пакет предлагает инструментальную подде­ ржку для представлений Razor, в том числе средс­ тво lntelliSense для встроенных дескриnторных вспомогательных классов, которые применяются в представлениях внутри приложения SportsStore Создание структуры папок Следующий шаг заключается в добавлении папок, которые будут содержать ком­ поненты, требуемые для приложения MVC: модели, контроллеры и представления. Чтобы создать папки , описанные в табл. 8.2, щелкните правой кнопкой мыши на эле­ менте проекта SportsStore в окне Solution Explorer (элемент внутри папки src), вы­ берите в контекстном меню пункт Addi:::>New Folder (Добавить~:::> Новая папка) и введите имя папки. Позже потребуются дополнительные папки, но создаваемые сейчас папки отражают главные части приложения МVС и для начала их вполне достаточно. Таблица 8.2. Папки, требующиеся для проекта SportsStore Имя Models Contro llers Views Описание Эта папка будет содержать классы моделей Эта папка будет содержать классы контроллеров Эта папка будет содержать все, что относится к представлениям, в том числе индивидуальные файлы Razor, файл запуска представле­ ния и файл импортирования представлений Конфигурирование приложения Приложение ASP.NET Core МVС полагается на несколько конфигурационных фай­ лов . Имея установл енные пакеты NuGet, понадобится отредактировать класс Startup, чтобы сообщить ASP.NET о том. что они должны использоваться (листинг 8 .2).
206 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Листинг 8.2. Включение средств в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . AspNetCore . Hosting ; using Microsoft.AspNetCore.Http; using Microsoft . Extensions . Dependencyinjection ; using Microsoft .Extensions.Logging ; namespace SportsStore { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); puЬl i c void Configure(IApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseМvcWithDefaultRoute(); Метод ConfigureServices применяется для настройки разделяемых объектов , которые могут использоваться повсеместно в приложении через средство внедрения зависимостей, которое рассматривается в главе 18 . Метод AddMvc (), вызываемый внутри метода ConfigureServices (), является расширяющим методом, который настраивает разделяемые объекты, применяемые в приложении MVC. Метод Configu re () используется для настройки средств, которые получают и обрабатывают НТГР-запросы . Каждый метод. вызываемый в методе Configure (), представляет собой расширяющий метод, который настраивает средство обработки НТГР-запросов (табл. 8.3). Таблица 8.3 . Начальные методы для настройки средств, вызываемые в классе Startup Метод UseDeveloperExceptionPage() UseStatusCodePages() UseStat i cFiles() UseMvcWithDefaultRoute() Описание Этот расширяющий метод отображает детали исключе­ ния , которое произошло в прило жении , что полезно во время процесса разработки . Он не дол жен быть вклю­ чен в развернутых приложениях; в главе 12 будет пока­ зано , как отключить данное средство Этот расширяющий метод добавляет простое сообще ­ ние в НТТР-ответы, которые иначе бы не имели тела, такие как ответы 404 - Not Found (404 - не найдено) Этот расширяющий метод включает поддержку для обслуживания статического содержимого из папки wwwroot Этот расширяющий метод включает инфраструктуру ASP.NET Core MVC со стандартной конфигурацией (которая позже в процессе разработки будет изменена)
Глава 8. SportsStore : реальное прило ж ение 207 На заметку! Класс Startup - важное средство ASP.NEТ Саге . Он будет подробно описан в главе 14. Дале е понадобится подготовить приложение для представлений Razor. Щелкните правой кнопкой мыши на папке Views, выберите в конт екстном меню пyнкт AddqNew ltem (Добавить q Новый элемент) и укажите шаблон MVC View lmports Page (Страница импортирования представлений MVC ) из категории ASP.NET (рис. 8 .3). AS!'.NEТ 1 1 Cl1enH ide Code '1 Online l!il" MVC V.<w l•yovt P•ge r:s: 12.1 MVC View Stм Page ASP.NEТ ASP.NE1 1 1 ~·~ ~z.crТ119 Hrlpe1 ASP.NEТ ~·fq Mi ddlew11re (iass ASP.NП ~·!ti Startup c l11~s ASP.NE1 vГJ ASP.NEТ Conligura tion File д\Р.NП 1 ы.-~ (li<> he<tlo go 011!one •nd l•ndtempl1tr1 c-~---v~~-~=:_~~------------- Se.шhlnS1all. ~ • Туре: ASP~ MVCView~ 1 ~ f ( " "1 f J Рис . 8.3. Создание файла импортирования представлений Щелкните на кнопке Add (Добавить), чтобы создать файл _Vi ew i mp orts . c s html и приведите его содержимое в соответствие с листинго м 8 .3 . Листинг 8.3. Содержимое файла_Viewimports. cshtml из папки Views @using Sports S tore . Models @addTagHelper * , Microsoft . AspNetCo r e . Mvc .T agHe l pers Оператор @using позволит применять типы и з пространства имен Sports S t ore . Model s в представлениях, не ссылаясь на это пространство имен. Оператор @addTagHelper включает встроенные дескрипторны е вспомогательные классы , ко­ торые будут использоваться позже для создания элементов HTML, отражающих кон­ фигурацию прил ожения SportsStore. Создание проекта модул ьного тестирования Создание про екта модульного тестирования требует выполнения процесса, кото­ рый был описан в главе 7. С помощью проводника файлов создайте папку по име ­ ни te s t на том же уровне, что и существующая папка s r c, внутри папки решения SportsStore . Возвратит есь в Visual Studio , щелкните правой кнопкой мыши на элементе ре­ шения SportsStore (элемент верхнего уровня в окне Solution Explorer), выберите в контекстно м меню пункт Add q New Solution Folder (Добавить q Новая папка решения) и установите имя новой папки в t est .
208 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Щелкните правой кнопкой мыши на папке test в окне Solution Explorer и выбери­ те в контекстном меню пункт Add~New Project (Добавить~Новый проект). Выберите шаблон Class Library (.NET Core) (Библиотека классов (.NET Core)) из категории lnstalled~Visual C#~.NET Core (УстановленныеqVisuаl C#q .NET Core), как показано на рис. 8.4, и установите имя проекта в SportsStore. Tests . \Neb .N_Er.~le; Android C1oud E.(tensiЬility iOS Reporti ng Silverlight Test WCF t- Online Name: Loc~tion: Console Applic'1 tion (.NЕТ Core} Visua\ C:: ASP.NEТ Core WebApplit.: ! tion (.N П Core) VisuaJ (:; S_po 11 sSto 1 ~Tбts !c:~roj!d,!~P~~~~l~~t,.~~~t,.,.~ ~:~.~ Рис. 8.4. Создание проекта модульного тестирования Щелкните на кнопке Browse (Обзор) и перейдите в папку test. Щелкните на кноп­ ке ОК, чтобы выбрать эту папку, и затем щелкните на ОК, чтобы создать проект мо­ дульного тестирования. После создания проекта модульного тестирования отредактируйте содержащийся в нем файл proj ее t . j son согласно листингу 8.4, и добавьте пакеты, необходимые для тестирования и создания имитированных объектов. На заметку! Пакет rnoq. ne tcore, который применяется в листинге 8.4, требует изменения конфигурации Visual Studio , как было описано в разделе "Добавление инфраструктуры имитации" главы 7. Если вы не прорабатывали примеры из главы 7, то должны внести это изменение в конфигурацию прямо сейчас. Листинг 8.4 . Содержимое файла proj ect. j son из проекта модульного тестирования "version ": "1.0.0- * ", " testRunner ": "xunit", "dependencies": { " Microso ft.NETCore.App": "type ": "platform ", "ver sion ": "1.0.0" }, " xunit": "2. 1. О ", " dotnet -test-xunit ": " 2 . 2 .0 -previ ew2 -build l02 9", "moq.net core ": "4.4. 0 -beta8 ",
Глава 8. SportsStore : реальное приложение 209 "S ystem .Diagnostics . TraceSource" : "4.0 .0", "SportsStore ": "1. 0 .0" )1 "frameworks ": { "ne t coreappl.O": "imports": ["dotnet5.6", "portaЫe-net 4 5+win8"] Проверка и запуск приложения Проекты приложения и модульного тестирова­ ния созданы, сконфигурированы и готовы к раз­ работке. Окно Solution Explorer должно содержать элементы, показанные на рис. 8.5. Если вы види­ те другие элементы или элементы расположены не в тех позициях, то возникнут проблемы, поэ­ тому уделите время проверке, что все элементы присутствуют и находятся на своих местах. Выбрав в меню Debug (Отладка) пункт Start Debugging (Запустить отладку) или Start Without Debugging (Запустить без отладки) , если вы пред­ почитаете итеративный стиль разработки, опи­ санный в главе 6, вы увидите страницу ошибки (рис. 8.6). Сообщение об ошибке отображается из-за того, что в настоящий момент в приложе­ нии нет контроллеров для обработки запросов; эту проблему мы вскоре решим. Solution bplortt ~Q ~ j Ф·:< iJ' jj''"- "~~~~1.~t~~!~:~f~~_!:~>- - "' ~~ Solution lttms lJ glob•l.j •on " ._ ~1( • iJ Sport<Store ~ }' Propert;e. ~ • ·8 Referenc6 0 1 ~V\W/fOOt • ·• Depмd tncie :-;; ,_ Controllers ~ Models " ~ Vit'\ 'JS (21 _Vie"нtmports .c-;h tn1I с• Progrмц s ~ 6J prcject.json ,!) Project_Readme.html с• Startup.cs v') v1eb.config .jf !i.. tt.st " ~S Sport.sStore.Tms flc /' Propertie\ i. • ·1 Referencб с- Clг~s1.cs. ~ /J project.j•on Р· ~~--·-·. ... . -· ~-· _... _ __,__ Начало работы с моделью предметной области Рис. 8.5. Окно Solution Explorer для проектов приложения SportsStore и модульного тестирования Все проекты начинаются с модели предмет- ной области, которая является центральной частью приложения МVС. Поскольку мы строим приложение электронной коммерции, то наиболее очевидная модель, кото­ рая необходима, касается товара. Добавьте в папку Models файл класса по имени Product . cs с определением, приведенным в листинге 8.5 . Листинг 8.5. Содержимое файла Product. cs из папки Models namespace SportsStore . Models { puЫic class Product { puЫic int ProductID { get ; set; puЫic string Name { get ; set; } puЫic string Description { get ; set; puЬlic decimal Price { get; set; } puЫic string Category { get ; set ; }
210 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC lnt~rn"1 S erv~r Error Х ----~~ : 1 Status Code : 404; Not Fou nd____ ----, ________J Рис. 8.6. Выполнение приложения SportsStore Создание хранилища Нам нужен какой-то способ получения объектов Product из базы данных. Как объяснялось в главе 3, модель включает в себя логику для сохранения и извлечения данных из постоянного хранилища. Пока можно не беспокоиться о том, как будет реализовано постоянство данных, но необходимо начать процесс определения ин­ терфейса для него . Добавьте в папку Model s новый файл интерфейса С# по имени I ProductRepository . cs и поместите в него определение, показанное в листинге 8.6. Листинг 8.6. Содержимое файла IProductReposi tory. cs из папки Models using Systern. Col lecti ons .Generic; narnespac~ SportsStore . Mod els { puЫ ic interface IProduc t Repos i tory IEnurnera Ы e<Product> Product s { get; Этот инте рфейс использует I E n urneraЫe < T > , чтобы позволить вызывающе­ му коду получать последовательность объектов Product , нич его не сообщая о том, как или где хранятся либо извлекаются данные. Класс, зависящий от интерфейса IProdu ctRepos i tory, может получать объекты Produ ct, ничего не зн ая о том, от­ куда они поступают или каким образом класс реализации будет их доставлять. В про­ цессе разработки мы еще будем возвращаться к интерфейсу IProduc tReposito r y , чтобы добавлять в него нужные средства . Создание фиктивного хранилища Теперь, когда определен интерфейс, можно было бы реализовать механизм пос­ тоянства и привязать его к базе данных, но сначала необходимо добавить ряд дру­ гих частей приложения. Для этого мы создадим фиктивную реализацию интерф ейса IProdu ctRepos i t o ry, которая будет замещать хранилище данных до тех пор, пока мы им не займемся . Чтобы со здать фиктивное хранилище , доб авьте в папку Models файл класса по имени FakeProductRepo si tor y . cs и поместите в него определени е, представленное в листинге 8.7 . Листинг 8. 7 . Содержимое файла FakeProductRepository. cs из папки Models us i ng System. Collections . Ge neri c ; namespace SportsStore . Model s { puЬl i c class FakeProductRepository I Pr oduc t Repos i tory {
Глава 8. SportsStore : реальное приложение 211 puЫic IEnumeraЫe<Produc t> Products => new List<Product> new Product { Name "Football", Price = 25 }, new Product { Name "Surf board", Price = 179 } , new Product { Name "Runn ing shoes", Price = 95 } }; Класс FakeProductReposi tory реализует интерфейс IProductReposi tory, воз­ вращая фиксированную коллекцию объектов Product в качестве значения свойства Products. Регистрация службы хранилища В МVС делается особый акцент на применении слабо связанных компонентов, что означает возможность вносить изменения в одну часть приложения, не внося соответс­ твующие изменения где-то в другом месте. Такой подход трактует части приложения как службы, которые предоставляют функциональные средства, используемые другими частями приложения. Класс, предоставляющий службу, впоследствии может быть мо­ дифицирован или заменен, не требуя внесения изменений в классы , которые его задейс­ твуют. Более подробно это объясняется в главе 18, а в случае приложения SportsStore необходимо создать слу;н:бу хранилища, которая позволит контроллерам получать ре­ ализующие интерфейс I ProductRep os i tory объекты, не зная, какой класс применя­ ется. В итоге появится возможность начать разработку приложения с использованием простого класса FakeProductRepository, созданного в предыдущем разделе, и позже заменить его реальным хранилищем, не внося изменения во все классы, которым ну;кен доступ в хранилище. Слу;кбы регистрируются в методе ConfigureServices () класса Startup; в листинге 8.8 определена новая службы ДJIЯ хранилища. Листинг В.В. Создание службы хранилища в файле Startup. cs using Microsoft . AspNetCore . Builder; using Microsoft.AspNetCore . Hosting; using Microsoft . AspNetCore . Http ; using Microsoft . Extensions.Dependencyinjection ; using Microsoft . Extensions.Logging; using SportsStore.Models; namespace SportsStore { puЫic class Startup { puЫic void ConfigureServi ces(IServiceCo llection services) { services.AddTransient<IProductRepository, FakeProductRepository>(); services . AddMvc() ; puЫic void Configure(IApplicationB uilder арр , IHostingEnvironment env, ILoggerFa ctory loggerFactory) { app . UseDeveloperExceptionPage(); app.UseStatusCodePages() ; app .Us eStaticFiles() ; app . UseMvcWithDefaultRoute();
212 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Добавленный в метод Confi g ure Servi ce s ( ) оператор сообщает инфраструкту­ ре ASP.NET о том, что когда компоненту наподобие контроллера н е обходима р е али­ зация интерфейса IProductRepository , она должна получить экземпляр класса FakeProdu ctReposi tory . Метод Ad d Transie nt () указывает, что каждый раз. когда требуется реализация интерфейса IProd uctRepository, должен создаваться новый объект FakeProductRepos i tory. Не беспокойт е сь, есл и смысл код а пока не особен ­ но понятен; вскоре вы увидите, как он вписывается в приложение. а детали того, что происходит, будут представлены в главе 18. Отображение списка товаров Оставшуюся часть главы можно было бы посвятить построению модели предм ет­ ной области и хранилища, вообще не касаясь других частей приложения. Однако та­ кой подход выглядит довольно скучным, поэтому мы пойдем другим путем и присту ­ пим к интенсивному применению MVC, вернувшись к добавлению средств модели и хранилища , когда они понадобятся . В этом разделе нам предстоит создать контроллер и метод действия , который мо­ жет отображать сведения о товарах в хранилище. Пока ими будут только данные из фиктивного хранилища, но позже мы решим эту задачу. Мы также подготовим на­ чальную конфигурацию маршрутизации, чтобы инфраструктур а MVC з нал а , каким образом сопоставлять запросы в приложении с контроллером , который будет создан. Использование формирования шаблонов MVC в Visual Studio Везде в книге контроллеры и представления MVC создаются за счет щелчка правой кнопкой мыши на папке в окне Solution Explorer, выбора в контекстном меню пункта Add<=:>New ltem (Добавить~Новый элемент) и указания шаблона элемента в открывшемся диалоговом окне Add New ltem (Добавление нового элемента). Существует альтернативный прием , называ ­ емый формированием шаблонов (scaffolding), при котором среда Visual Studio предлагает в контекстном меню Add (Добавить) пункты, специально предназначенные для создания контроллеров и представлений . Выбор таких пунктов меню способствует выбору сценария для создаваемого компонента, такого как контроллер с действиями , допускающими чтение / запись, или представление, которое содержит форму, применяемую для создания специфи­ ческого объекта модели. В настоящей книге формирование шаблонов не используется . Код и разметка , генериру­ емые средством формирования шаблонов, являются настолько общими, что едва ли не бесполезны , в то время как набор поддерживаемых сценариев ограничен и не решает рас­ пространенные задачи разработки . Цель настоящей книги - не только донести до вас зна­ ния, каким образом создавать приложения MVC, но также объяснить, как все работает "за кулисами" , и сделать это гораздо труднее, когда ответственность за создание компонентов возлагается на средство формирования шаблонов. Тем не менее, это еще одна ситуация, когда ваш стиль разработки может отличаться от мое­ го , и вы вполне можете предпочесть работу со средством формирования шаблонов. В таком случае можете включить его, сделав ряд добавлений в файле p r oj ect . j son. Во-первых, в разделе dependencies потребуется указать два новых пакета: " dependen cies ": { "Microsoft.NETCore .App ": " version": "1.0 .0", " type": "pl atform" }'
}, Глава 8. SportsStore : реальное nрило ж ен и е 213 " Microsoft . AspNetCore . Di agnostics ": "1. 0 . 0 ", " Microsoft .AspNetCore . Server .IISintegr ati on": "1. 0 . О ", " Microsof t. AspNetCore . Server . Kes t rel ": "1.О.О", " Microsoft . Extensions . Logging . Console ": " 1 . 0.0 ", "Microsoft.AspNet Core .Razor .Tool s ": { }1 "version": "l.0 .0-preview2-final", " type ": " build " "Microsoft.AspNetCore . Stati cFiles ": "1 .О . О", " Microsof t. AspNetCore . Mvc ": "1.О.О", "Microsoft.VisualStudio.Web.CodeGeneration.Tools": "version" : 11 1 . О. 0-preview2-final", 11 type 11 : 11 build11 }, 11 Мicrosoft.VisualStudio.Web.CodeGenerators.Mvc 11 : 11 version 11 : 11 1. О. O-preview2-final 11 , "type": "build" Во-вторых, эти пакеты должны быть зарегистрированы в разделе tools : "tools": { }, 11 Microsoft .AspNetCore .Razor . Tool s ": "l.0 .0 - preview2- final 11 , "Microsoft .AspNetCore . Server . IISintegr ation . Too l s ": " l . 0 . 0 - preview2 - final", "Microsoft. VisualStudio. Web. CodeGeneration. Tools 11 : { "version 11 : 11 1 . О . O-preview2-final 11 , "imports 11 : [ 11 portaЫe-net45+win8+dnxcore50 11 , 11 portaЫe - net45+win8" После сохранения изменений и установки пакетов средой Visual Studio вы увидите новые пункты в контекстных меню, открываемых по щелчку правой кнопкой мыши на папках в окне Solution Explorer. Выбор этих пунктов меню будет приводить к появлению диалоговых окон, позволяющих выбирать сценарии , которые должны применяться для создания контроллера или представления. Добавление контролле ра Чтобы создать п ервый контроллер в приложении, добавьте в папку Con t r ol lers файл класса по им ени ProductController . c s с опр едел е нием, показанным в лис­ тинге 8.9. Листинг 8.9. Содержимое файла ProductController. cs из папки Controllers using Microsoft . AspNetCore . Mvc ; using SportsStore .Mode l s ; namespace SportsStore . Controllers
214 Часть 1. Введение в инфраструктуру ASP.NET Core MVC puЫic class ProductController : Controller private IProductRepository repository; puЫic ProductController(IProductRepository repo) { repository = repo; Когда инфраструктуре MVC необходимо создать новый экземпляр клас­ са ProductController для обработки НТГР-запроса, она проинспектирует конструктор и выяснит, что он требует объекта, который реализует интерфейс IProductRepository. Чтобы определить. какой класс реализации должен исполь­ зоваться, инфраструктура МVС обращается к конфигурации в классе Startup, ко­ торая сообщает о том, что необходимо применять класс FakeRepository, а также о том, что каждый раз должен создаваться его новый экземпляр. Инфраструктура MVC создает новый объект FakeReposi tory и использует его для вызова конструктора ProductController с целью создания объекта контроллера, который будет обраба­ тывать НТТР-запрос. Такой подход известен под названием внедрение зависимостей и позволяет объек­ ту ProductController получать доступ к хранилищу приложения через интерфейс IProductRepository без необходимости в знании того, какой класс реализации был сконфигурирован. Позже мы заменим фиктивное хранилище реальным, а благодаря внедрению зависимостей контроллер продолжит работать безо всяких изменений. На заметку! Некоторым разработчикам не нравится внедрение зависимостей, поскольку они считают, что оно усложняет приложения. Я не придерживаюсь такой точки зрения, но если вы не знакомы с внедрением зависимо стей, то рекомендую вам дождаться главы 18, пре­ жде чем п ринимать решение. Далее добавьте метод действия по имени List (),который будет визуализировать представление, отображающее полный список товаров из хранилища (листинг 8.1 О). Листинг 8.10. Добавление метода действия в файле ProductController. cs using Microsoft . AspNetCore . Mvc; using SportsStore . Models; namespace SportsStore.Controllers puЫic class ProductController : Controller private IProductRepository repository; puЫic ProductController(IProductRepository repo) { repository = repo; puЬlic ViewResult List() => View(repository.Products); Вызов метода View () подобного рода (без указания имени представления) указы ­ вает инфраструктуре MVC о том, что нужно визуализировать стандартное представ­ ление для метода действия. Передача методу View () экземпляра List<Product> (списка объектов Product) снабжает инфраструктуру данными, которыми необходи ­ мо заполнить объект Model в строго типизированном представлении.
Глава 8. SportsStore: реальное приложение 215 Добавление и конфигурирование представления Нам нужно создать представление для отображения содержимого пользователю, но потребуется проделать ряд подготовительных шагов, чтобы упростить написание представления. Сначала необходимо создать разделя емую компоновку, в которой бу­ дет определено общее содержимое, включаемое во все отправляемые клиентам НТМL­ ответы . Разделяемые компоновки - удобный способ обеспечить согласованность представлений и наличие в них важных файлов JavaScript и таблиц стилей CSS; их работа объяснялась в главе 5. Создайте папку Views /Shared и добавьте в нее новую страницу компоновки пред­ ставлений MVC по имени Layou t. cshtml, которое является стандартным именем, назначаемым средой Visual Studio элементу такого типа. В листинге 8. 11 приведено содержимое файла_Layout . cshtml. В стандартное содержимое внесено одно изме­ нение , связанное с установкой внутренностей элемента ti tle в SportsStore . Листинг 8.11. Содержимое файла _Layout. cshtml из папки Views/Shared < ! DOCTYPE html> <html> <11 ead> <meta name= " viewport " content= " width=d evice-width " /> <title>SportsStore</title> </head> <body> <div> @RenderBody () </div> </body> </html> Далее пон адобится сконфигурировать прилож ение, чтобы файл _L ayout . cshtml прим енялся по ум олчанию . Это делается добавлением в папку Views файла с шаблоном MVC View Start Page (Ф айл запуска представления MVC) и именем ViewStar t . cshtml. Стандартное содержимое, добавляемое Visual Studio (листинг 8.12), выбирает ком­ поновку по имени _Layout . cshtml , которая соответствует файлу из листинга 8 . 11 . Листинг 8.12. Содержимое файла_ViewStart. cshtml из папки Views @{ Layout " Layout "; Теперь необходимо добавить представлени е, 1юторое будет отображаться, 1\ОГ­ да для обработки запроса используется метод действия List () . Создайте папку Views/Product и добавьте в нее файл представления Razor по имени Li st. cshtml. Поместите в него разметку. показанную в листинге 8 . 13 .
21 б Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Листинг 8.13. Содержимое файла List. cshtml из папки Views/Product @model IEnumeraЬle<Product> @foreach (var р in Model) { <div> <hЗ>@p . Name</hЗ> @p .D escription <h4>@p .P rice .T oS tr ing( " c ") </h4> </div> Выражение @rnodel в начале файла указывает, что представл ение будет получать от метода действия последовательность объектов Product в качестве данных модели. С помощью выражения @foreach осуществля ет ся проход по этой последовательнос ­ ти и генерация простого набора НТМL-элементов для 1шждого полученного объекта Product . Представлению не известно, откуда поступили объекты Product, как они были получены или охватывают ли они все товары, известные приложению. Взамен пред­ ставление имеет дело только с тем , как отображать детали каждого объекта Product с применением НТМL-элементов, что согласуется с принципом разделения обязаннос­ тей, который был описан в главе 3. Совет. Значение свойства Р r i се преобразуется в строку с использованием метода ToString ( " с "), который визуализирует числовые значения в виде денежных зна­ чений в соответствии с настройками культуры, действующими на сервере . Например , если в качестве настройки культуры сервера установлено en -u s, тогда вызов (1002 . 3) . ToString ( " с " ) возвратит значение $1 , 002. 30, но если для сервера ус­ тановлена культура en -GB , то этот же вызов возвратит значение f. 1 , 002 . 30 . Установка стандартного маршрута Инфраструктуре MVC понадобится сообщить, что она должна отправлять запросы, поступающи е для корневого URL приложения (h t tp: //мой-сайт/ ), методу действия List () класса ProductController. Это делается путем редактирования оператора в классе Startup, который настр аивает iuraccы МVС, обрабатыв аю щие НТТР-запросы (листинг 8.14). Листинг 8.14. Изменение стандартного маршрута в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions . Dependencylnjection ; using Microsoft . Extensions . Logging ; using SportsS tore . Models ; namespace SportsStore { puЫic class Startup { puЬlic void ConfigureServices( I ServiceCollection services) { services.AddTransient<IProductRepository, FakeProductRepository>() ; services.AddMvc() ;
Глава 8. SportsStore: реальное прило же ние 217 puЫic void Configure(IApplicationBuilder а рр, IHost ingEnvironment env, ILoggerFa ctory loggerFactory) { app .Us e Develop erExc ept i on Pag e() ; app . UseStatusCodePages(); app . UseSta t icFiles( ) ; арр. UseМvc (routes => { routes .мapRoute ( name: "default", template: "{controller=Product}/{action=List}/{id?}"); }); Метод Configure ( ) класса Startup применяется для настройки 1юнвейера за­ просов, состоящего из классов (известных как промежуточное программное обеспе­ чение), которые будут инспектировать НТГР-запросы и генерировать ответы. Метод UseMvc () настраивает пром ежуточное программное обеспечение МVС, причем одним из параметров конфигурации является схема, которая будет использоваться для со­ поставления URL с контроллерами и методами действий. Система маршрутизации подробно рассматривается в главах 15 и 16, а пока просто следует знать, что измене­ ния, выделенные в листинге 8 . 14 . указывают инфраструктуре MVC на необходимость отправки запросов методу действия List () контроллера Product, если только в URL запроса не указано иное. Совет. Обратите внимание, что в листинге 8.14 имя контроллера указано как Product , а не ProductController , являющееся именем класса. Это часть соглашения об именова­ нии MVC, в рамках которого имена классов обычно заканчиваются словом Controller , но при ссылке на класс данная часть имени опускается. Соглашение об именовании и его влияние объясняются в главе 31. Запуск приложения Все основные компоненты в наличии. Мы имеем контроллер с методом действия, который MVC будет применять, когда запрашивается стандартный URL для прило­ жения. Инфраструктура MVC создаст экземпляр класса FakeRepository, после чего будет использовать его для создания нового объекта контроллера, обрабатывающего запрос. Фиктивное хранилище снабдит контроллер простыми тестовыми данными, которые его метод действия передаст представлению Razor, так что НТМL-ответ для браузера будет включать детали каждого товара. При генерации НТМL-ответа инфра­ структура МVС объединит данные из представления, выбранные методом действия. с содержимым разделяемой компоновки, порождая завершенный НТМL-документ, который браузер в состоянии разобрать и отобразить. Запустив приложение, можно увидеть результат, показанный на рис. 8. 7 . Это типовой шаблон разработки для инфраструктуры ASP.NET Core MVC. Начальные затраты времени на необходимую настройку являются обязательными. но затем базовые средства приложения будут собир аться очень быстро.
218 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 1·. SportsStor~ Х 1 f- ' С [Ф lo~host:бOOOO --------===----==::::::=::=.:::=--_-:::::::::::.:.. -~ Football S25.00 JI Su1·f boa1·d Sl79.00 lR1шnlng slloes $95.00 - -- -- -- -- - ---- --- ---- --- ---- -·- --- ---- ---- Рис. 8.7. Просмотр основной функциональности приложения Подготовка базы данных Мы способны отображать простое представление, содержащее сведения о товарах, но применяем тестовые данные, которые находятся в фиктивном хранилище. Прежде чем можно будет реализовать хранилище с реальными данными, необходимо настро ­ ить базу данных и заполнить ее данными. Для базы данных будет использоваться SQL Server, а доступ к ней будет осущест­ вляться с применением Entity Framework Core (EF Core) - инфраструктуры объект­ но-реляционного отображения (object-relational mapplng - ORM) для Microsoft .NET . Инфраструктура ОRМ представляет таблицы, столбцы и строки реляционной базы данных посредством обычных объектов С#. На заметку! Это область, где можно выбирать из широкого диапазона инструментальных средств и технологий. Доступны не только различные реляционные базы данных, но мож­ но также работать с хранилищами объектов, хранилищами документов и рядом экзотичес­ ких альтернатив. Кроме того, существуют другие инфраструктуры ORM для .NET, каждая из которых использует слегка отличающийся подход; такие вариации позволяют выбрать то, что лучше всего подойдет для ваших проектов. Инфраструктура Entity Framework Core применяется по нескольким причинам: ее просто запустить в работу, она прекрасно интегрирована с LINQ (мне нравится ис­ пользовать LINQ) и она хорошо работает с ASP.NET Core MVC. Ранним выпускам этой инфраструктуры были присущи мелкие недостатки, но текущие версии элегантны и богаты возможностями. Продукты Visual Studio и SQL Server располагают удобным средством LocalDB, ко­ торое представляет собой не требующую администрирования реализацию основной функциональности SQL Server, специально спроектированную для разработчиков. Благодаря этому средству можно пропускать процесс настройки базы данных на вре­ мя построения проекта , а развертывание проводить в полном экземпляре SQL Server позже. Большинство приложений MVC развертываются в размещаемых средах, ко­ торые обслуживаются профессиональными администраторами, и наличие средства Loca!DB означает, что конфигурирование баз данных остается в руках администрато­ ров баз данных, а разработчики приложений могут заниматься написанием кода.
Глава 8. SportsStore: реальное прило же ние 219 Совет. Если при установке Visual Studio вы не выбрали LocalDB , то придется сделать это сейчас. Средство представляет собой часть инструментов для работы с данными или же его можно установить как часть SQL Server. Установка Entity Framework Core Инфраструктура Entity Framework Core устанавливается с применением NuGet, а в листинге 8.15 показаны требуемые добавления в раздел dependencies файла proj ect . j son в проекте SportsStore. Листинг 8.15. Добавление Entity Framework Core в файле proj ect. j son внутри проекта SportsStore "dependencies ": )' " Microsoft . NETCore . App ": "version": "1.0 . 0 ", " type ": " platform " )' " Microsoft . AspNetCore.Diagnostics ": "1. 0 . 0 ", " Microsoft.AspNetCore . Server . I ISintegration": " 1 . 0 . 0 ", " Microsoft . AspNetCore .Server. Kestrel ": " 1.0 . 0 ", " Microsoft . Extensions . Logging . Con s ole ": "1. 0.0 ", " Microsoft . AspNetCore . Razor . Tools" : ( )' " version ": "l.0. 0 - preview2 -final ", " type ": " build" " Microsoft . AspNetCore . StaticFiles ": " 1. О. О ", " Microsoft . AspNetCore . Mvc ": "1. 0 .0", "Мicrosoft.EntityFrameworkCore. SqlServer" : "1. О. 0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final" Базы данных управляются с использованием инструментов командной строки. ко­ торые настраиваются в разделе tools файла project . j son (листинг 8.16). Листинг 8.16. Регистрация инструментов EF Core в файле proj ect. j son внутри проекта SportsStore " tools ": )' " Microsoft . AspNetCore . Razor . Tools": "1. 0 . 0-preview2-final ", "Mi crosoft . AspNetCo re.Server. IISintegration.Tools ": "1. 0 .0 -preview2- final", "Мicrosoft . EntityFrameworkCore. Tools": { "version": "1.0 .0 -preview2-final", "imports": [ "portaЫe-net45+win8+dnxcore50", "portaЬle-net45+win8" ]
220 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC После сохранения файла proj ect. j son среда Visual Studio загрузит и установит инфраструктуру EF Core, а также добавит ее в проект. Создание классов базы данных Класс контекста базы данных является шлюзом между приложением и EF Core, обеспечивая доступ к данным приложения с применением объектов моделей. Чтобы создать класс контекста базы данных для приложения SportsStore, добавьте в папку Models файл класса по имени ApplicationDbContext. cs с определением, приве­ денным в листинге 8 . 17. Листинг 8.17 . Содержимое файла ApplicationDЬContext. cs из папки Models using Microsoft .EntityFrameworkCore ; namespace SportsStore.Models { puЫic class Applicat i onDbContext : DbContext { puЫic ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base (options) {} puЫic DbSet<Product> Products { get; set; } Базовый класс DbContext предоставляет доступ к лежащей в основе функцио ­ налыюсти Entity Fraшework Core, а свойство Products обеспечивает доступ к объек­ там Product в базе данных. Для наполнения базы данных добавьте в папку Models файл 1-шасса по имени SeedData. cs и поместите в него определение, показанное в листинге 8.18. Листинг 8.18. Содержимое файла SeedDa ta. cs из папки Model s using System .Linq; using Microsoft .AspNetCore.Builder; using Microsoft . Extensions .Dependencyinjection; namespace SportsStore.Models { puЫic static class SeedData { puЫic static void EnsurePopula ted(IApplicationBuilde r арр) ApplicationDbContex t context = app.ApplicationServices .GetRequiredService<ApplicationDbCon text>(); if (!context .Products .Any()) { context.Products.AddRange( new Product { Name = "Kayak", Description = "А boat for one person", Category = " Watersports ", Price = 275 }, new Product { Name = " Lifejacket ", Description = "Protect ive and fashionaЫe", Category = "Wat ersports ", Price = 48. 95m } , new Product { Name = "Soccer Ball ", Description = "FIFA-approved size and weight", Category = "Soccer", Price = 19. 50m },
Глава 8. SportsStore: реальное прило ж ение 221 new Product { Name = " Corne r Flags ", Description = "Give your pl aying field а professional touch", Categor y = " Soccer", Price = 34. 95m }, new Product { Name = " Stadium", Description = " Fl at- packed 35, 000 - seat stadi um ", Category = " Soccer", Price = 79500 }, new Product { Name = " Thinking С ар ", Description = "Improve brain ef ficien cy Ьу 75%", Category = "Chess", Price = 16 }, new Product { Name = "Unst eady Chair", Descri ption = "Secr etly give your oppone nt а disadvantage", Category = " Chess", Price = 29.95m }, new Product { Name = " Human Chess Board", Description = "А fun game for t he family", Category = "Chess", Price = 75 }, new Product { } Name = "Bling-Bling King", Des c r ipt ion = "Gold-plated, diamond-s t udd ed King", Ca tegory = "Che ss", Price = 1200 ); conte x t . SaveChanges (} ; Статический метод Ens ureP op ul ated () получает аргумент типа IApp licat i o n Bu i lder, который является нлассом, используемым в методе Con figure () нласса Startup при регистрации классов промежуточного программного обеспечения для обработки Н1ТР-запросов; именно здесь будет обеспечиваться наличие содержимого в базе данных. Метод The EnsurePopu l a t ed () получает объект Appl ica ti o n DbCon t ex t пос­ редством интерфейса IApp lica tionBu i lder и применяет его для проверки , при­ сутствуют ли в базе данных какие-нибудь объекты Produc t. Если объектов нет, то база данных наполняется с использованием коллекции объектов Product и метода AddRange () ,после чего сохраняется с помощью метода Sav eCha n ge s () . Создание класса хранилища Хотя в настоящий момент может показаться иначе , но большая часть работы, тре­ буемой }J)IЯ настройки базы данных , завершена. Следующий шаг заключается в со­ здании класса, который реализует интерфейс IProd u c t Reposi tory и получает дан­ ные с применением инфраструктуры Entity Framework Core. Добавьте в папку Mo d els файл класса по имени EFProductRe p os i to ry. cs с определением класса хранилища, представленным в листинге 8.19.
222 Часть 1. Введение в инфраструктуру ASP. NET Core MVC Листинг 6.19. Содержимое файла EFProductReposi tory. cs из папки Models using System. Collecti ons .Generic; n a mespace Spo rts Store . Models { puЫic clas s EFPr o d uctRepos i to r y : IProductRepos i tory { pr i vat e App licationDbContext context ; puЫic EFP r oductRepository(Appl icat i o n DbContext ctx) { context = ctx; puЫ i c IEnumeraЫe<Produc t > Produ ct s => context . Products ; По мере добавления ср едств к приложению ~тасс будет дополняться новой фун­ кциональностью, но пока что ре ализация хранилища просто отображает сво йство Products, определенно е в инт е рфейсе IPr odu ctRepo s i tory, на свойство Pro ducts , которое определено в классе Appl i cati onDbContext . Определение строки подключения Строка подключения указывает местоположени е и имя базы данных , а также предоставля ет конфигурационные настройки, с которыми приложени е долж но под­ ключаться к серверу базы данных. Строки подключ ения хранятся в файле JSON под названием apps etti n g s. j son, который создан в проекте SportsStor e с исполь зова­ ни ем шаблона элемента ASP.NET Configuration File (Конф и гурационный файл ASP.NET) из раздела ASP.NET диалогового окна Add New ltem. При создании файл а appset t ings . j son среда Visual Studlo добавля ет в н е го за ­ полнитель для строки подключения, который необходимо привести в соотв етствие с ЛИСТИНГОМ 8 .20. Листинг 8.20. Редактирование строки подключения в файле appsettings. j son "Data": { " Spor tStoreProd ucts ": " Con n ectionStr i ng ": " Server=( l ocaldb)\\MSSQLLoca l DB; Database=SportsStore ; Trusted_Connection=True ; MultipleAct i v e ResultSets =true " } Вну три раздела Da ta конфигурационного файла имя строки подключ е ния уста­ навлива ется в SportsStoreProdu cts . Значени е элемента Conn ectionString ука­ зывает, что для базы данных по имени Sp ort s Store должно прим еняться ср едство Loca!DB . Совет. Строка подключения должна быть выражена в виде единственной неразрывной строки кода, что нормально для редактора Visual Studio , но не умещается на печатной странице и приводит к неуклю жему форматированию в листинге 8 .20 . При определе­ нии строки подключения в собственном проекте удостоверьтесь, что значение элемента Connect i onStri ng на ходится в единственной строке кода.
Глава 8. SportsStore : реальное прило ж ение 223 Конфигурирование приложения Дал ее понадобит ся прочитать строку подключения и сконфигурировать приложе­ ние для ее использования при подключении к базе данных. Для чтения строки под­ ключения из файла appsettings . j son требуется еще один пакет NuGet. В листин­ ге 8.21 показано изменение раздела dependencies внутри файле proj ect . j son . Листинг 8.21. Добавление пакета в файле project. json проекта SportsStore " dependencies ": }' " Microsoft . NETCore . App ": "version": "1.0 .0 ", " type ": " platform " }' " Microsoft . AspNetCore . Diagnostics ": "1. О . О ", "Microsoft .AspNetCore . Server .I ISintegrati on ": "1 . 0 .0", "Microsoft.AspNetCore . Server. Kestrel": "1 .О . О", "Microsoft.Extensions . Logging .Console": "1 .0 .0 ", " Microsof t. AspNetCore . Razo r. Tools ": { }' " version ": " l.0 .0 -preview2-final", " type" : "build" " Microsoft . AspNetCore. Sta t icFi l es " : "1. О. О ", " Microsoft . AspNetCore. Mv c": "1 .О.О", " Microsoft . EntityFramewor kCore . SqlServer": " 1.0 . 0 ", " Microsoft . EntityFrameworkCore . Tools ": " l . 0 . 0 - preview2 - final ", "Мicrosoft.Extensions.Configuration.Json": "1.0 .0" Этот пакет делает возможным чтени е конфигурационных данных из файлов JSON, таких как appsett i ngs . j son . Чтобы применить фун к циональность , предлагаемую новы м пакетом , для чтения строки подключения из конфигурационного файла и что­ бы настроить EF Core , в класс Startup н е обходимо внести соответствующее измене­ ние (листинг 8.22). Листинг 8.22 . Конфигурирование приложения в файле Startup. cs using Microsoft.AspNetCore . Builder ; using Mic r osoft . AspNetCore . Host i ng ; using Microsoft . AspNetCore . Http ; using Microsof t . Extensions . Dependencyinj ect i o n; using Microsoft . Extensions . Logging ; using Spor tsStore . Mode l s ; using Мicrosoft.Extensions.Configuration; using Мicrosoft . EntityFrameworkCore; namespace SportsStore { puЫic clas s Startup { IConfigurationRoot Configuration; puЬ1ic Startup(IHostingEnvironment env) { Configuration = new ConfigurationBuilder()
224 Часть 1. Введение в инфраструктуру ASP.NET Core MVC .SetBasePath(env.ContentRootPath) .Ad dJsonFile("appsettings.json") .Build(); puЫic void Conf igu reServices(IServiceCollect ion services) services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreProducts:ConnectionString"])); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddMvc(); puЬlic void Configure(IApplication Builder арр , IHostingEnvironment env , ILogger F actory loggerFactory) { app . UseDeve l operExceptionPage() ; app.UseStatusCodePages(); app.UseStaticFiles(); app .U seMvc(routes => { routes.MapRoute( name : " default ", template : "{ controller = Product)/ {action =Li st}/{id?} " ) ; }); SeedData . EnsurePopulated(app); Добавленный в класс Startup конструктор загружает конфигурационные на­ стройки из файла appsettings . j s on и делает их доступными через свойство по имени Configuration. Особенности чтения и доступа к конфигурационным данным рассматриваются в главе 14. В метод ConfigureServices () добавл е на последовательность обращений к мето­ дам, которая настраивает инфраструктуру Entity Framework Core . services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration["Data:SportStoreProducts:ConnectionString"])); Расширяющий метод AddDbContext () настраивает службы, предоставляемые инфраструктурой Entity Framework Core для класса контекста базы данных. кото­ рый был создан в листинге 8.17. Как объяснялось в главе 14, многие методы, ис­ пользуемые в классе Startup, позволяют конфигурировать службы и средства про­ межуточного программного обеспечения с применением аргументов с параметр ами . Аргументом метода AddDbContext () является лямбда-выражение , которое получает объект options, конфигурирующий базу данных для класса контекста. В этом случае база данных конфигурируется с помощью метода UseSqlServer (} и указания стро­ ки подключения, которая получена из свойства Configuration. Еще одно изменение, внесенное в класс Startup, было связано с заменой фиктив­ ного хранилища реальным: services . AddTransient<IProductRep ository , EFProductRepository>();
Глава 8. SportsStore: реальное приложение 225 Компоненты в приложении, использующие интерфейс IProductRepository, к которым в настоящий момент относится только контроллер Product, при создании будут получать объект EFProductReposi tory, предоставляющий им доступ к инфор­ мации в базу данных. Подробные объяснения будут даны в главе 18, а пока просто знайте, что результатом будет гладкая замена фиктивных данных реальными из базы данных без необходимости в изменении класса ProductController. Финальное изменение в 1тассе Startup касается вызова метода SeedDa ta . EnsurePopul ated (). который гарантирует наличие в базе данных определенной тестовой информации и вызывается внутри метода Configure () класса Startup. При запуске приложения метод Startup. ConfigureServices () вызывается пе­ ред методом Startup . Configure (). т. е. ко времени вызова метода SeedData . EnsurePopulated () можно иметь уверенность в том, что службы Entity Framework Core уже установлены и сконфигурированы. Создание и применение миграции базы данных Инфраструктура Entity Framework Core способна генерировать схему для базы данных, используя классы моделей, с помощью средства, которое называется мигра­ циями. При подготовке миграции инфраструктура EF Core создает класс С#, содер­ жащий команды SQL, которые нужны для подготовки базы данных. Если необходимо модифицировать классы моделей, тогда вы можете создать новую миграцию, которая содержит команды SQL, требуемые для отражения изменений. Таким образом, вам не придется беспокоиться о написании вручную и тестировании команд SQL, и вы можете сосредоточиться на классах модели С# в приложении. Команды EF Core выполняются с применением консоли диспетчера пакетов (Package Manager Console), которая открывается через пункт меню Toolsq NuGet Package Manager (СервисqДиспетчер пакетов NuGet) в Visual Studio. Запустите в консоли диспетчера пакетов следующие команды, чтобы создать класс миграции, 1юторый подготовит базу данных к первому использованию: Add-Migration Initial Когда команда завершит свое выполнение, вы увидите в окне Solution Exp lorer сре­ ды Visual Studio папку Migrations. Именно в ней инфраструктура Entity Framework Core хранит классы миграции. Одно из имен файлов будет выглядеть как длинное число с _ Initial. cs после него, и это класс, который будет применяться для созда­ ния начальной схемы базы данных. Просмотрев содержимое такого файла , вы увиди­ те, как класс модели Product использовался для создания схемы. Запустите приведенную ниже команду, чтобы создать базу данных и выполнить команды миграции: Update-Database Создание базы данных займет какое-то время, но после завершения работы ко­ манды вы сможете увидеть результат, запустив приложение. Когда браузер запраши ­ вает стандартный URL для приложения, !{онфигурация приложения сообщает МVС о необходимости создания контроллера Product для обработки запроса. Создание кон­ троллера Product означает вызов конструктора класса ProductController, которо­ му требуется объект, реализующий интерфейс I Р roduct Repo s i tor у, и новая конфи­ гурация указывает MVC о том, что для этого должен быть создан и применен объект EFProductReposi tory. Объект EFProductRep osi tory обращается к функциональ­ ности EF Core , которая загружает реляционные данные из SQL Server и преобразует
226 Часть 1. Введение в инфраструктуру ASP.NET Core MVC их в объекты Product. Вся упомянутая работа скрыта от класса ProductController, который просто получает объект, реализующий интерфейс IProductRepository, и пользуется данными, которые он предоставляет. В итоге окно браузера отображает тестовую информацию из базы данных (рис. 8.8). 1·. \po. - ts\to« х С @i-1oc_ a _ lh-os-t.600-0- = 0===- - - ·- -- !· -- . =-- --=::-=-=-=-----=---===--··--- 1~ ' 1 Kayak 1 А b~ar for оп< p<rso11 1 5275.00 1 LlfejAckN Prot~ Ct1\·~ n11d f(lsluo111\ЬI~ 1 1 S4S.95 1 \ SoccпBnll ' LElfЛ:<"1oe_ro ~Oя.z.~_fI04..!~~i~1t_________________________~-- · --- "' Рис. В.В. Использование хранилища в виде базы данных Такой подход с применением Entity Framework Core для представления базы дан­ ных SQL Server в виде последовательности объектов моделей отличается простотой и легкостью, позволяя сосредоточить все внимание на инфраструктуре ASP.NET Core MVC. Я опустил множество деталей , связанных с функционированием EF Core, и большое количество доступных конфигурационных параметров . Мне нравится инф­ раструктура Entity Framework Core, и я рекомендую уделить время на ее изучение. Хорошей отправной точкой послужит веб-сайт Microsoft для Entity Framework Core, находящийся по адресу http: / /ef. readthedocs. io . Добавление поддержки разбиения на страницы На рис. 8.8 видно, что представление List . cshtml отображает сведения о товарах в базе данных на одной странице. В этом разделе мы добавим поддержку разбиения на страницы, чтобы представление отображало на странице меньшее число товаров, а пользователь мог переходить со страницы на страницу для просмотра всего каталог. В метод действия List () контроллера Product необходимо добавить параметр, как показано в листинге 8.23. Листинг 8.23. Добавление поддержки разбиения на страницы в метод действия List () в файле ProductController. cs using Microsoft.AspNetCore . Mvc ; using SportsStore.Models ; using System.Linq; namespace SportsStore . Controllers puЬlic class ProductController : Controller private IProductRepository repository;
Глава 8. SportsStore : реальное прило же ние 227 puЬlic int PageSize = 4; puЫic ProductController(IProdu c t Repository repo) { repository = repo ; puЬlic ViewResult Li s t ( int page = 1) => View(repository.Products .OrderBy(p => p.ProductID) . Skip((page - 1) * PageSize) .Take(PageSize)) ; Поле PageSize указывает, что на одной странице должны отображаться сведения о четырех товарах. Позже мы заменим его более совершенным механизмом. В метод List () добавлен необязательный параметр. Это означает, что в случае вызова метода без параметра (List () )вызов обрабатывается так, словно ему было передано значение, указанное в определении параметра (Li st ( 1) ). В результате метод действия отобража­ ет первую страницу сведений о товарах, когда инфраструктура МVС вызьmает его без аргумента. Внутри тела метода действия мы получаем объекты Product, упорядочи­ ваем их по первичному ключу , пропускаем товары, которые распол агаются до начала текущей страницы, и выбираем количество товаров , указанное в пол е PageSi ze. Модульное тестирование: разбиение на страницы Модульное тестирование средства разбиения на страницы можно провести , создав имитированное х ранилище, внедрив его в конструктор класса ProductController и вызвав метод List () , чтобы запрашивать конкретную страницу. Затем полученные объе кты Product можно сравнить с теми, которые ожидались от тестовы х данны х в ими­ тированной реализации. Написание модульных тестов обсуждалось в главе 7. Ни же пока­ зан созданный для этой цели модульный тест, который находится в файле класса по имени ProductControl l erTes ts . cs, добавленном в проект Spor tsSt ore . Tests: using System.Collections . Generic ; using System . Linq ; using Moq ; using SportsStore.Controllers ; using SportsStore .M odels ; using Xuni t ; namespace SportsStore . Tests puЬlic class ProductControllerTests [Fact] puЫic void Can Paginate() 11 Организация Mock<IProductRepository> mock = new Mock<IProdu ctRepository>() ; mock . Setup(m => m. Products) .Returns(new Product[] { new Product {ProductID 1, Name "Pl"}, new Product new Product new Product new Product }); {ProductID = 2 , {ProductID = З , {ProductID 4 ' {ProductID = 5 , Name Name Name Name = "Р2"}, "РЗ"), "Р4"), " PS"} ProductController cont roller = new ProductController(mock . Object);
228 Часть 1. Введение в инфраструктуру ASP.NET Core MVC controller . PageSize = З ; 11 Действие IEnumeraЫe<Product> result controller .Li st(2) . ViewData . Mode l as IEnumeraЫe<Product> ; 11 Утверждение Product[] prodArray = result.ToArray(); Assert . True(prodArray .Length == 2); Assert.Equal("Р4", prodArray[О] .Name) ; Assert . Equal( " PS ", prodArray[l] . Name) ; Получение данных, возвращаемых из метода действия, выглядит несколько неуклюже. Результатом является объект ViewResul t, и значение его свойства ViewData . Model должно быть приведено к ожидаемому типу данных . В главе 17 рассматриваются разнооб­ разные результирующие типы, которые могут возвращать методы действий, а также спосо ­ бы работы с ними. Отображение ссылок на страницы Запустив приложение, вы увидите, что теперь на странице отображаются четыр е позиции каталога. Если нужно просмотреть другую страницу, в конец URL можно до­ бавить параметр строки запроса, например: http : //localhost : 60000/ ?page=2 Вы должны указать в URL номер порта, который был назначен вашему проекту. Используя такие строки запросов, можно перемещаться по каталогу товаров. У пользователей нет какого-либо способа выяснить о существовании этих парамет ­ ров строки запроса, но даже если бы он был , то вряд ли бы они захотели иметь дело с навигацией подобного вида. Взамен мы должны визуализировать в нижней части каждого списка товаров ссылки на страницы, чтобы пользователи могли переходить между страницами. Для этого мы реализуем дескрипторный вспомогательный класс, который будет генерировать НТМL-разметку для требуемых ссылок. Добавление модели представления Чтобы обеспечить поддержку дескрипторного вспомогательного класса, мы соби­ раемся передавать представлению информацию о количестве доступных страниц, текущей странице и общем числе товаров в хранилище. Проще всего это сделать, создав класс модели представления, который специально прим ен яется для пер еда­ чи данных между контроллером и представлением. Создайте в проекте SportsStore папку Models/ViewModels и добавьте в нее файл класса с содержимым, приведен­ ным в листинге 8.24 . Листинг 8.24. Содержимое файла Paginginfo. cs из папки Models/ViewModels using System ; namespace SportsStore . Models . ViewModels puЫic class Paginginfo { puЫic int Totalitems { get; set ; }
Глава 8. SportsStore : реальное прило ж ение 229 puЫic int ItemsPerPage { get ; s et ; } puЫic int CurrentPage ( get; set ; } puЫic int TotalPages = > (int)Math . Cei l ing((decima l )Tot alitems / I t e msPe r Page) ; Добавление дескрипторного вспомогательного класса Те п е рь , имея мод ел ь пр едставл е ния, можно создать дес1<рипторный вспомогатель ­ ный класс . Создайте в проекте Sport sS tore папку I nfrastr u cture и добавьте в нее файл класса по им ени P ageLinkTagHel p er . cs с определением, показанным в лис­ тинг е 8.25. Де скрипторны е вспомогательные классы являются крупной частью инф­ раструктуры ASP.NET Core MVC, и в главах 23-25 объясняется , как они работают и каким образом их создавать. Совет. В папку Infrastructure будут помещаться классы , которые предоставляют прило­ жению связующий код, но не имеют отношения к предметной области приложения . Листинг 8.25. Содержимое файла PageLinkTagHelper. cs из папки Infrastructure using Microsoft . AspNetCore . Mvc ; using Microsoft . AspNetCore . Mvc .Rende r ing ; using Microsoft . Asp NetCo r e . Mvc.Rout ing ; using Microsoft . AspNetCore . Mvc . Vi e wFeatures ; u sing Microsoft . AspNetCore . Ra z or . Ta gHelp ers ; using SportsStore . Mode l s . ViewMode l s ; namespace SportsStore . Infrastructure [HtmlTargetElement( " div ", Attribu t es = " page -model " )] puЫic class PageLinkTagHelper : TagHe l per { pr i vate IUrlHelperFactory url Helpe rFactory ; puЫic PageLinkTagHelper(IUr l He l perFactory helperFactory) u rlHel perFactor y = help erFactor y; [ViewContext] [HtmlAttributeNotBound] puЬlic ViewContext ViewContex t { get ; set ; puЫic Paginginfo PageMode l { get ; set ; } puЫic string PageAction { get ; set ; } puЫic override v oid Process( TagHelp e r Co n t e xt co n text , TagHelperOutput output) ( IUrlHelper urlHelper = ur l HelperFactory.GetUrlHelper(ViewCont ext) ; TagBuilder res ul t = new TagBuilder( " div " ) ; for (int i = 1; i <= PageModel .Tot alPages; i++) { TagBuilder tag = new TagBuilder ( " а " ) ; tag .Attributes ["href"] = url Hel per .Action (PageAction, new { page = i }) ; tag . Inn erHtml .Append (i.ToSt ring() ); result . InnerHtml . AppendHtml(tag) ; output . Content . AppendHtml(re s u l t . InnerHtml ) ;
230 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Этот дескрипторный вспомогательный класс заполняет элемент di v элементами а , которые соответствуют страницам товаров. Сейчас мы не будем вдаваться в ка­ кие-либо детали относительно дескрипторных вспомогательных классов; достаточно знать, что они предлагают один из наиболее удобных способов помещения логики С# в представления. Код для дескрипторного вспомогательного класса может выглядеть запутанным, потому что смешивать С# и HTML непросто. Но использование деСJ{рип­ торных вспомогательных классов является предпочтительным способом включения блоков кода С# в представление, поскольку дескрипторный вспомогательный класс можно легко подвергать модульному тестированию. Большинство компонентов MVC, таких как контроллеры и представления , об­ наруживаются автоматически, но дескрипторные вспомогательные классы долж­ ны быть зарегистрированы. В листинге 8.26 приведено содержимое файла _ Viewirnports . cshtrn l из папки Views с добавленным оператором, который сооб­ щает MVC о том, что дескрипторные вспомогательные классы следует искать в про­ странстве имен SportsStore. Infrastructure. Кроме того, добавлено также вы­ ражение @using, чтобы на классы моделей представлений можно было ссылаться в представлениях, не указывая пространство имен . Листинг 8.26. Регистрация дескрипторного вспомоi-ательного класса в файле_Viewimports. cshtml @using SportsStore . Models @using SportsStore.Models.ViewModels @addTagHelper * , Microsoft . AspNetCore . Mvc . TagHelpers @addTagHelper SportsStore.Infrastructure .*, SportsStore Модульное тестирование: создание ссылок на страницы Чтобы протестировать дескрипторный вспомогательный класс PageLinkTagHelper, вызывается метод Process () с тестовыми данными и предоставляется объект TagHelperOutput , который инспектируется на предмет сгенерированной НТМL­ разметки . Тест определен в новом файле PageLinkTagHelperTests. cs внутри проекта SportsStore. Tests: using System.Collections.Generic; using System . Threading . Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore . Mvc . Routing; using Microsoft.AspNetCore . Razor . TagHelpers; using Moq; using SportsStore.Infrastructure; using SportsStore.Models . ViewModels ; using Xunit ; namespace SportsStore . Tests { puЫic class PageLinkTagHelperTests [Fact] puЬlic void Can_Generate Page Links() 11 Организация var urlHelper = new Mock< IUrlH e lper>() ;
Глава 8. SportsStore : реальное прило ж ение 231 urlHelper.SetupSequence(x => x . Action(It . IsAny<UrlActionContext>())) . Returns( 11 Test/Pagel 11 ) . Returns( 11 Test/Page2 11 ) . Returns( 11 Test/PageЗ 11 ); var urlHelperFactory = new Mock<IUrlHelperFactory>() ; urlHelperFactory . Setup(f => f . GetUrlHelper(It . IsAny<ActionContext>() )) . Returns(urlHelper.Object) ; PageLi nkTagHelper helper = new PageLinkTagHelper(urlHelperFactory . Object) PageModel = new Paginginf o { CurrentPage = 2 , Totalitems = 28 , I t emsPerPage = 10 }, PageAction = 11 Test 11 }; TagHelperContext ctx = new TagH e lperContext( new TagHelperAttributeList() , new Dictionary<object , object>(), 1111 ); var content = new Mock<TagHelperContent>() ; TagHelperOutput output = new TagHe l perOutput( 11 div 11 , new TagHelperAttributeList() , (cache , encoder) => Task.FromResult(content.Object)); 11 Действие helper . Process(ctx , output) ; 11 Утверждение Assert . Equal(@ 11 <a href= 11 " Test/Pagel "" >l</a> 11 + @11<а href= 1111 Test/Page2 1111 >2</a> 11 + @ 11 <а href= "" Test/Page3 1111 >З</a> ", output . Content . GetCon t ent() ) ; Сложность этого теста связана с созданием объектов, которые требуются для создания и применения дескрипторного вспомогательного класса . Дескрипторные вспомогательные классы используют объекты IUr lHelperFactory для генерации URL, которые указывают на разные части приложения, и для создания реализаций этого интерфейса и связанного с ним интерфейса IUrlHelper , предоставляющего тестовые данные , используется инф­ раструктура Moq. Основная часть теста проверяет вывод дескрипторного вспомогательного класса с приме­ нением литерального строкового значения, которое содержит двойные кавычки. Язык С# позволяет работать с такими строками при условии, что строка предварена символом @, а вместо одной двойной кавычки внутри строки используется набор из двух двойных кавычек ( 1111 ) . Вы должны помнить о том , что разносить литеральную строку по нескольким строкам файла нельзя , если только строка , с которой производится сравнение, не разнесена ана­ логичным образом . Например, литерал, применяемый в тестовом методе, был размещен в нескольки х строках из-за недостаточной ширины печатной страницы . Символ новой строки не добавлялся, иначе тест не прошел бы.
232 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Добавление данных модели представления Мы пока не готовы использовать дес1(рипторный вспомогательный класс, т. к. пред­ ставление еще не снабжено экзе мпляром класса модели представления Paginginfo. Это можно было бы сделать с применением объекта Vi ewBag, но лучше пом е стить вс е данные , подлежащие пер едаче из контроллера в представление , внутрь единствен­ ного класса мод ели предст авления. Добавьте в папку Mode l s/ViewMo d e l s про е кта Sp ortsS t o r e ф айл класса по имени P r oductsListViewModel . cs с содержимым . приведенным в листинг е 8 .27. Листинг 8.27 . Содержимое файла ProductsListViewModel. cs из папки Models/ViewModels using System.Collections.Generic; using SportsStore.Model s; n a mespace SportsStore . Mode l s . Vi ewMode l s p uЫic c l ass Products ListViewModel { puЫic IEnumeraЫe<Product> Products get ; set ; } puЫic Paginginfo Paginginfo { get ; set; } Теперь можно обновить метод действия Li s t () кл асса Produc t Co n troller так. чтобы он использовал ю~асс Pr odu cts Li s tVi ewMo d el для снабжения представления сведениями о товарах, отображаемых на страницах. и информацией о разбиении на страницы (листинг 8.28). Листинг 8.28. Обновление метода действия List() в файле ProductController.cs using Mi crosoft . Asp NetCore . Mvc ; u sing SportsStore . Mode l s ; using System.Li nq; using SportsStore.Models.ViewModels; n amespace SportsSt ore . Cont roll ers { puЬlic cl ass ProductControll er : Controll er p rivate IProd u c t Repository repository ; puЫic int PageSize = 4; puЫic ProductController(I Produ ctReposit ory re p o) { r eposi tory = repo; puЫic ViewRes ult List(int page = 1) => View(new ProductsListViewModel Products = repository.Products . OrderBy(p => p.ProductID) . Skip ( (page - 1) * PageSize) . Take(PageSize), Paginginfo = new Paginginfo CurrentPage = page, ItemsPerPage = PageSize, } }); Totalitems = repository.Products . Count()
Глава 8. SportsStore : реальное прило ж ение 23 3 Внесенные изменения обеспечивают передачу представлению объекта Products ListViewModel как данных м одели . М одульное тестиро вание: данные разбиен и я н а страницы для модели пр едставления Нам необходимо удостовериться в то м , что контроллер отправляет представлению коррек­ тную инфор м ацию о разбиении на страницы . Ни же по казан модульный тест, добавленный в класс ProductControllerTests внутри тестового прое кта , который обеспечивает та кую проверку : [Fact] puЫic void Can_Send_ Pagination_View_Model() { 11 Организация Mock<IProductRepository> mock = new Mock<IProduc t Repository>() ; mock.Setup(m => m. Products) . Returns(new Product[] { new Product {ProductID 1 , Name "Pl"), new Product {Product ID 2, Name "Р2"} , new Product {ProductID new Pr oduct { Product I D new Product {Product I D )); 3, Name 4, Name 5, Name 11 Организация ProductControl l er controlle r = "РЗ"), "Р4"}, "Р5") new ProductCon troller(mock . Object) { PageSize 3); 11 Действие ProductsListVi ewModel result = controller . List(2) . ViewData.Model as Products ListViewModel ; 11 Утверждение Paginginfo pageinfo = result . Pagingin f o ; Assert . Equal(2 , pageinfo . CurrentPage) ; Assert . Equal(З , pageinfo .I temsPerPage) ; Assert . Equal(5 , pageinfo . Tota l items) ; Assert . Equal(2 , pageinfo.TotalPages) ; Потребуется также модифицировать ранее созданный модульный тест для проверки раз­ биен и я на страницы, содер жащийся в методе Can_Paginate () .Он полагается на метод действия List () , возвращающий объе кт ViewRe s ul t , свойством Model которого явля ­ ется последовательность объе ктов Product, но мы поместили эти данные внутрь еще од­ ного типа м одели представления. Вот переделанный тест: [E'act ] puЫic void Can_Paginate() { 11 Организация Mock<IProductRepository> mock = new Mock<IProduct Repository>() ; mock . Setup(m => m. Products) . Re t urns(new Product[J { new Product {Product I D 1 , Name "Pl"), new Product { ProductID = 2 , Name = "Р 2 "),
234 Часть 1. Введение в инфраструктуру ASP.NET Core MVC new Product {ProductID = 3 , Name new Product {ProductID = 4 , Name new Product {ProductID = 5 , Name }); "Р3"}, "Р4"}, " PS"} ProductControl ler controller = new ProductController(mock . Object) ; controller.PageSize = 3 ; 11 Действие ProductsListViewModel result = controller.List(2) .ViewData.Model as ProductsListViewModel; //У твержде ние Product[] prodArray = result.Products.ToArray(); Assert . True(prodArray . Length == 2); As sert.Equa l("P4 ", prodArray(OJ . Name) ; Assert . Equal("P5 ", prodArray[l] . Name) ; Учитывая объем дублированного кода в двух тестовых методах, обычно следовало бы со­ здать общий метод начальной установки. Однако, поскольку модульные тесты описаны в индивидуальных врезках вроде этой, их код представлен по отдельности, чтобы с каждым тестом можно было ознакомиться независимо от других. В настоящий момент представление ожидает последовательность объектов Product , поэтому файл List . cshtml необходимо обновить, как показано в листин­ ге 8.29, чтобы иметь дело с новым типом модели представления. Листинг 8.29. Обновление файла List. cshtml @model ProductsListViewModel @foreach (var р in Model.Products) <div> <h3>@p .N ame</ h3> @p . Description <h4>@p . Price.ToStr ing( " c " )</h4> </div> Выражение @mo de l было изменено для указания механизму Razor на то, что те­ перь мы работаем с другим типом данных. Цикл foreach был обновлен, чтобы источ­ ником данных стало свойство Products объекта модели. Отображение ссылок на страницы Наконец, все готово для добавления в представление List ссылок на страницы. Мы создали модель представления, которая содержит информацию о разбиении на страницы. обновили контроллер, чтобы эта информация передавалась представле ­ нию, и модифицировали выражение @model , приведя его в соответствие с новым типом модели представления . Осталось только добавить НТМL- элемент, который де ­ скрипторный вспомогательный класс будет обрабатывать для создания ссылок на страницы (листинг 8.30).
Глава 8. SportsStore: реальное приложение 235 Листинг 8.30 . Добавление ссылок на страницы в файле List. cshtml @mode l Prod u cts ListVi ewMode l @foreach (var р i n Model .P r odu cts) <div> <h3>@p . Name</h3> @p . Description <h 4>@p . Pri ce .ToStri ng( "c" )</h4> </div> <div page-model="@Model.Paginginfo" page-action="List"></div> Запустив приложение, вы увидите новые ссылки на страницы (рис. 8.9). Стиль отображения довольно примитивен, но далее в главе он будет улучшен. Пока важно то, что ссыл ки позволяют перемещаться по страницам каталога и знакомиться с то­ в а рам и, выставл е нными на продажу. Когда механизм Razor обнаруживает атрибут page - model в элементе d i v , он обращается к классу PageLinkTagHelper для пр е­ образования элемента, что и дает набор ссьmок, показанных на рис. 8.9 . 1 Produш )( 1 Produщ Х 1 1 Products Х ... ,. . -- ------ ------ \ +- -' С D localhost:60000/'pag _ -- -· ·------ +- . . , С С1 localhost:бOOOO/?page•З '{;;1= · f- ----- +- -" С D locall1os t 60000/>page• ------------------ ! Koyok 1----~- -- '- --- -"' -' -- - Bll11g-Bli11g Кlug StadillШ 1 А boat fot онео pcr~o11 ! Gold -pla.tcd duiшoш1-snIOdcd Кшg 1 5275 • .ОО IFlat-packcd ЗS.000-~c<ct !iiti'ldшш SI ,20 0.00 1 1 S79 ,500 .00 1 Lifфckel lli Tbluki11g Сор j Prot ectt\'C' анd t"n11>!1ioщ1ЬJ 1 1 1 Iщр1·0 \·е bri'\bl cfficict1cy Ь S4S.9S j Slб.00 Soccr1· Bo ll 1 1 UnsleodY Chзi1· FIFA-npproп•d si z tшd \\'tigltt 1 . Stc rC'tly giп· ущ1r 1 Sl 9.SO j S29.95 Cш·nei·Fl " s 1 Huшnn Cl1 )ii'IY111? , fiC 'ld а prof~.<.:s tOJl~I to\j А fш1 gаш or tl1 e fa1шly 1 575 .00 -- ·-L :::J 1 -·- Рис. 8.9 . Отображение ссылок для навигации по страницам На заметку! Если вы запустите приложение с применением пункта меню Start Debuggiпg (Запустить отладку), то можете столкнуться с сообщением об ошибке, предупреждающим о том, что коллекция была модифицирована. Это дефект EF Core, который дол ж ен быть исправлен к моменту выхода настоящей книги, но если он все же останется, тогда про­ стая перезагрузка окна браузера решит проблему и приведет к отображению содержимо­ го, представленного на рис . 8.9 .
236 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Почему бы просто не воспользоваться элементом управления GridView? Если вы ранее работали с ASP.NET, то вам может показаться , что такой большой объем ра­ боты привел к довольно скромным результатам. Пришлось написать немало кода лишь для того, чтобы получить список товаров с разбиением на страницы. Если бы мы прибегли к услугам инфраструктуры Web Forms, то такого же результата можно было бы достичь с при­ менением готового элемента управления Gr i dV i ew или Li s tVi ew из ASP.NET Web Forms, привязав его напрямую к таблице Pr o d ucts базы данных. То, что было сделано в главе, выглядит не слишком впечатляюще, но серьезно отличается от процесса перетаскивания какого-то элемента управления на поверхность визуального проектирования. Во-первых, мы строим приложение с надежной и удобной в сопровожде­ нии архитектурой, которая задействует подходящее разделение обязанностей . В отличие от простейшего использования Lis t View мы не связываем напрямую пользовательский интерфейс и базу данных, что представляет собой подход, который дает быстрые резуль­ таты, но вызовет проблемы и сложности в будущем. Во-вторых, по ходу дела мы создаем модульные тесты, что позволяет проверять поведение приложения естественным образом, который практически невозможен в случае применения сложного элемента управления Web Forms . Наконец, в-третьих, не забывайте, что значительная часть главы посвящена созда­ нию инфраструктуры, на основе которой строится приложение. Например, определить и реализовать хранилище нужно только один раз, и теперь, когда оно имеется в нашем рас­ поряжении, новые функциональные средства можно строить и тестировать легко и быстро, как будет продемонстрировано в последующих главах. Разумеется, ничто из сказанного отнюдь не умаляет значимость безотлагательных результа­ тов, которые способна предложить инфраструктура Web Forms, но, как объяснялось в главе 3, такая безотлагательность имеет свою цену, которая может оказаться высокой и болезнен­ ной в крупных и сложных проектах. Улучшение URL Ссылки на страницы работают, но для передачи информации серверу они по-пре ­ жнему используют строку запроса, подобную следующей: http : //localhost/?page=2 Чтобы получить бол е е привлекательные URL, необходимо создать схему. которая следует шаблону компонуемых URL. Компонуемый URL - это URL, имеющий с мысл для пользователя, такой как показанный ниже: http : // l ocalhost/Page2 Инфрастру1пура МVС позволяет легко изменять схему URL в приложении, потому что применяет средство мapшpymuзa4uuASP. NET, которо е отвеча ет за обработку URL для выяснения, на какую часть приложения они указывают. Понадобится лишь доба­ вить новый маршрут при регистрации промежуточного программного обеспеч ения МVС в методе Configure () класса Start up (листинг 8.31). Листинг 8.31. Добавление нового маршрута в файле Startup. cs puЫic void Confi gur e(IAppl icationBuil der арр, I HostingEnvir onment env , ILogg e r Factory l oggerFactor y) { app .UseDevel operExcept ionPage(); app .U seStatusCod ePages() ;
app.UseSta ticFi l es() ; app .U seMvc(routes => { routes .мapRoute ( name: "pagination", template: "Products/Page{page}", Глава 8. SportsStore : реальное приложение 237 defaults: new { Controller = "Product", action = "List" }) ; routes.MapRoute( name: "default", template : " {control l er=Product}/{act ion=List}/{id?} " ) ; }); SeedData.EnsurePopulated(app); Важно поместить этот маршрут перед стандартным маршрутом (по имени defaul t ). который уже присутствует в методе. Как будет показано в главе 15 , система маршрутизации обрабатывает маршруты в порядке их перечисления, а нам нужно , чтобы новый маршрут имел преимущество перед существующим. Это единственное изменение, которое потребуется внести, чтобы изменить схему URL для разбиения на страницы списка товаров. Инфраструктура MVC тесно интег­ рирована с функцией маршрутизации, поэтому приложение автоматически отражает изменение подобного рода в URL, используемых приложением , что касается и URL, которые генерируются дескрипторными вспомогательными классами вроде применя­ емых для гене рации ссылок на страницы . Не беспокойтесь, если маршрутизация пока не особенно понятна - она будет подробно рассматриваться в главах 15 и 16. Запустив приложение и щелкнув на ссылке для какой-нибудь страницы, вы уви­ дите новую схему URL в действии (рис. 8.10). Sta(liШll Flat-pnc ke<I 35.000 -seat stacl iщ11 1 ~"0.00 . ~· ~ " ....,. !11! .... ,.,,,....- ~..-..;...,..., _"'-___,,,. Рис . 8.10. Новая схема URL , отображаемая в браузере Стилизация содержимого Мы построили значительную часть инфраструктуры и базовые средства прило­ жения готовы к сборке всего воедино, но пока совершенно не уделяли внимание его внешнему виду. Хотя дан ная книга не посвящена веб-дизайну или CSS, внешний вид приложения SportsStore настолько примитивен, что это умаляет его технические до ­ стоинства. В настоящем разделе мы постараемся исправить ситуацию. Мы собираем­ ся реализовать классическую компоновку, содержащую два столбца и заголовок, как показано на рис. 8.11 .
238 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Home • Product 1 ·• Watersports • Product 2 • Socce.r • • Chess , (main body) •... .,,· Рис. 8. 11 . Целевой дизайн для приложения SportsStore Установка пакета Bootstrap Для предоставления стилей CSS, которые будут применены к приложению, мы планируем использовать пакет Bootstrap. При установке пакета Bootstrap мы будем полагаться на встроенную в Visual Studio поддержку инструмента Bower. Выберите шаблон элемента Bower Configuration File (Файл конфигурации Bower) из категории Client-side (Клиентская сторона) в диалоговом окне Add New ltem , чтобы создать в проекте SportsStore файл по имени bower. j son, как демонстрировалось в главе 6. Добавьте в раздел dependencies созданного файла пакет Bootstrap, как показано в листинге 8.32. Листинг 8.32. Добавление пакета Bootstrap в файле bower. j son внутри проекта SportsStore " name ": " asp.net", "private": true, "dependencie s": { "bootstrap": "3. 3. 6 11 После сохранения внесенных в файл bower. j son изменений среда Visual Studio применяет инструмент Bower для загрузки пакета Bootstrap в папку wwwroot/liЬ/ bootstrap. Пакет Bootstrap зависит от пакетаjQuегу. который также автоматически добавится в проект. Применение стилей Bootstrap к компоновке В главе 5 было показано, как работают представления Razor, как они используют­ ся и как встраивать в них компоновки. Файл запуска представления, добавленный в начале главы , указывает, что файл по имени_Layout. cshtrnl должен выступать в качестве стандартной компоновки, так что начальную стилизацию Bootstrap мы при­ меним именно к нему (листинг 8.33).
Глава 8. SportsStore : реальное приложение 239 Листинг 8.33. Применение СSS-файла Bootstrap к файлу _Layout. cshtml из папки Views/Shared <!DOCTYPE htrnl> <htrnl> <head> <rneta narne ="viewport " content= " width=devi ce - width " /> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> <title>Spo rtsStore </t itle> </head> <body> <div class="navbar navbar-inverse" role="navigation"> <а class="navbar-brand" href="#">SPORTS STORE</a> </div> <div class="row panel"> <div id="categories" class="col-xs -3"> Put something useful here later </div> <div class="col-xs-8"> @RenderBody () </div> </div> </body> </html> Элемент l in k имеет атрибут asp - href - include, который представляет собой пример и спользования встроенного дескрипторного вспомогательного класса. В дан­ ном случае дескрипторный вспомогательный класс просматривает значение атрибута и генерирует элементы link для всех файлов по указанному пути, который может содержать групповы е символы. Это удобное средство гарантирования того, что вы можете добавлять и удалять файлы из структуры папок wwwroot , не нарушая работу приложения , но , как объясняется в главе 25, указание групповых символов требует особого внимания , чтобы они соответствовали нужным файлам . Добавление в компоновку таблицы стилей CSS из Bootstrap означает, что оп­ ределенные в ней стили можно применять в любом представлении, которое пола ­ га ется на данную компоновку. В листинге 8.34 стилизация используется в файле List . cshtml. Листинг 8.34. Стилизация содержимого в файле List. cshtml @model Products ListViewMod e l @foreach (var р in Model . Products) <div class="well"> <hЗ> <strong>@p.Name</strong> <span class="pull-right laЬel laЬel-primary"> @р. Price. ToString ("с") </span> </hЗ> <span class="lead">@p.Description</span> </div>
240 Часть 1. Введение в инфраструктуру ASP.NET Core MVC <div page-model="@Model.Paginginfo" page-action="List" page-classes-enabled="true" page-class="Ьtn" page-class-normal="ьtn-default" page-class-selected= 11 Ьtn-primary 11 class= 11 Ьtn-group pull-right"> </div> Нам необходимо стилизовать кнопки, которые генерирует класс PageLin kTagHe 1ре r , но жестко встраивать классы Bootstrap в код нежелательно , т.к. это затруднит пов­ торное использование дескрипторного вспомогательного класса где-то в другом месте приложения либо изменение внешнего вида кнопок. Взамен мы определяем специ­ альные атрибуты в элементе di v, которые указывают требуемые классы. Данные ат ­ рибуты соответствуют свойствам, добавленным в дескрипторный вспомогательный класс, которые затем применяются для стилизации создаваемых эл ементов а (лис­ тинг 8.35). Листинг 8.35. Добавление классов к генерируемым элементам в файле PageLinkTagHelper.cs using Microsoft . AspNetCore . Mvc ; using Microsoft . AspNetCore . Mvc.Rendering ; using Microsoft . AspNetCore . Mvc . Routing ; using Microsoft.AspNetCore.Mvc . ViewFeatures; using Microsoft . AspNetCore . Razor .T agHe l pers ; using SportsStore.Models . ViewMode ls; namespace SportsStore.Infrastructure [HtmlTargetElement("div", Attributes = " page -model " )] puЫic c l ass Page LinkTagHe l per : TagHelper { private IUrlHelperFactory ur lH e lperFactory ; puЫic PageLinkTagHelper(IUrlHelperFactory helperFactory) urlHelperFactory = he lperFact ory ; [ViewContext] [HtmlAttributeNotBound] puЬlic ViewContext ViewCo n text { get ; set ; puЫic Paginginfo PageModel { get; set ; puЫic string PageAction { get ; set; } puЫic Ьооl PageClassesEnaЬled { get; set; puЬlic string PageClass { get; set; } puЬlic string PageClassNormal { get; set; } puЬlic string PageClassSelected { get; set; false; puЫic override void Process(TagHelperContext con t ext , TagHelperOutput output) { IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); TagBuilder result = new TagBuilder("div"); for (int i = 1 ; i <= PageModel . TotalPages ; i++) { TagBuilder tag = new TagBuilder("a"); tag.Attributes[ " href "J = ur lHelper.Action(PageAction , new{page= i});
Глава 8. SportsStore: реальное nриложение 241 if (PageClassesEnaЫed) { tag.AddCssClass(PageClass); tag.AddCssClass(i == PageМodel.CurrentPage ? PageClassSelected : PageClassNormal) ; tag .Inne r Html. Ap p end(i .T oS tring () ); r esul t .InnerHtml .AppendHtml(tag ) ; o utput. Co n tent . Appe ndHtml (result.In n erHtml); Значения атрибутов автоматически используются для установки значений свойств в дескрипторном вспомогательном классе, учитывая отображение между форматом имени атрибута HTML (p age - clas s-norma l) и форматом имени свойства С# (PageClassNormal) . Это позволяет дескрипторным вспомогательным классам по­ разному реагировать в зависимости от атрибутов НТМL-элемента, обеспечивая более гибкий способ генерации содержимого в приложении МVС. Запустив приложение , вы увидите , что его внешний вид улучшился - во всяком случае, в како й-то степени (рис. 8.12). !) Spo~•Stolf х с Cc?..~~.:i~·_:~~Qip;;,~~![P,1g~~--=~-=.=-===·===·=:~~-~J SPOПTS STOHE . jPut something uselul jt1 eie later Blin g-Bl ing Кing Gold·pl ated, diamond -studded Кing 1 1 2 ll l· ~Щ···-~~~~~~~~!~':~. ' Рис. 8.12. Приложение SportsStore с улучшенным дизайном Создание части чного представления В качестве завершающего штриха в данной главе мы проведем рефакторинг при­ ложения , чтобы упростить пр едставление Li s t . c s html. Мы создадим частичное представление, являющееся фрагментом содержимого , которое можно внедрять в другое представление подобно шаблону. Частичные представления подробно рас­ сматриваются в главе 21. Они помогают сократить дублирование , когда одно и то же содержимое должно появляться в разных местах приложения. Вместо того чтобы ко­ пировать и вставлять одинаковую разметку Razor во множество представлений, мож­ но определить ее единожды в частичном представлении. Чтобы создать частичное представление, добавьте в папку Vi ews/Sh a red файл представления Razor по имени Produ ctSumm ary . cs h tml и поместите в него разметку, показанную в листинге 8 .36 .
242 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 8.36 . Содержимое файла ProductSummary. cshtml из папки Views/Shared @model Product <div class= " well " > <hЗ> <strong>@Model.Name</strong> <span c l ass= " pull-right label label - primary"> @Model . Price . ToString( "c" ) </span> </hЗ> <span class="lead " >@Model . Description</span> </div> Теперь необходимо модифицировать файл List. csh tml из папки Views/ Products, чтобы в нем применялось частичное представление (листинг 8 .37). Листинг 8.37. Использование частичного представления в файле List. cshtml @model ProductsListViewModel @foreach (var р in Model . Products) @Html. Partial {"ProductSununary", р) <div page - model="@Model . Paginginfo" page-action="List " page - classes-enaЬled="true " page-class="Ьtn " page - class -normal = "Ьtn- default " page-class - selected= " Ьtn - primary"class="Ьtn-group pull-right " > </div> Мы взяли код разметки, который ранее размещался в цикле foreach в представ­ лении List. cshtml, и перенесли его в новое частичное представление. Обращение к частичному представлению производится с помощью вспомогательного метода Html . Partial () , которому в аргументах передаются имя представления и объект модели представления . Подобного рода переход на частичное представление является рекомендуемым приемом, поскольку он позволяет вставлять одну и ту же разметку в любое представление, которое нуждается в отображении сводки о товаре. Как видно на рис. 8 . 13, добавление частичного представления не изменяет внешний вид при­ ложения; оно просто меняет место, где механизм Razor находит содержимое, которое применяет для генерации ответа, отправляемого браузеру. Резюме В настоящей главе была построена основная инфраструктура для приложения SportsStore. Пока что она не содержит достаточного набора функциональных средств, чтобы их прямо сейчас можно было продемонстрировать пользователю , но "за кули­ сами" уже имеются зачатки модели предметной области с хранилищем товаров , кото­ рое поддерживается SQL Server и Entity Framework Core. В приложении присутствует единственный контроллер ProductController, который может создавать разбитые на страницы списки товаров, а также настроена ясная и дружественная к пользо ва­ телям схема URL.
Глава 8. SportsStore: реальное nриложение 243 С Spo1tsStore Х ~ с i_~-'~::'h_?:~6:_~~p:~~~?~~9~~~~~-~~-~==~===~~---------~_:_--=_~- -= ~~ .::€J SPORTS SТOilE · 1 IPut sornething LJseful lhere later Kayak А boat ror 0 11е person Рис. 8.13. Использование частичного представления В этой главе могло показаться , что для получения весьма незначительных выгод пришлось провести излишне трудоемкую начальную подготовку, но в следующей главе равновесие будет восстановлено. Теперь, когда фундаментальная структура на месте, можно двигаться дальше и добавлять все средства, ориентированные на поль­ зователя: навигацию по категориям, корзину для покупок и начальную форму для оплаты.
ГЛАВА 9 SportsStore: навигация в этой главе мы продолжим построение примера приложения SportsStore. В пре ­ дыдущей главе была добавлена поддержка для навигации по приложению и для начала создания корзины для покупок. Добавление навигационных элементов управления Приложение SportsStore станет намного удобнее , если пользователи получат воз­ можность навигации среди товаров по категории. Это будет делаться в три этапа. • Расширение метода действия List () в классе ProductControl l er , чтобы он был способен фильтровать объекты Product в хранилище. • Пересмотр и расширение схемы URL. • Создание списка иатегорий. иоторый будет находиться в боковой панели сай­ та, подсвечивая текущую категорию и предоставляя ссылки на остальные категории. Фильтрация списка товаров Мы начнем с расширения класса модели представления ProductsListViewModel, который был добавлен в проект Spo rtsS tore в предыдущей глав е . Нам нужно обес­ печить взаимодействие текущей категории с представлением, чтобы визуализировать боковую панель. и это хорошая отправная точка. В листинге 9.1 показаны изменения, внесенные в файл ProductsListViewModel . cs из папки Models/ViewModels . Листинг 9.1. Добавление свойства в файле ProductsListViewModel. cs using System.Collections .G eneric ; using SportsStore.Models ; namespace SportsStore . Models .V iewModels puЫic class ProductsListViewModel { } puЫic IEnumeraЬle<Product> Products get ; set ; } puЫ i c Paginginfo Paginginfo { get; set ; puЫic string CurrentCategory { get; set; } В класс ProductsListViewModel добавлено свойство по имени CurrentCategory. Следующий шаг заключается в обновлении класса ProductController, чтобы метод
Глава 9. SportsStore: навигация 245 действия List () фильтровал объекты Produ ct по к атегории и использовал только что добавленное в модель представления свойство дл я указания категории, выбран­ ной в текущи й момент. Изменения приведены в листинге 9.2. Листинг 9.2 . Добавление поддержки категорий к методу действия List () в файле ProductController. cs using Microsoft . AspNetCore . Mvc ; using SportsStore . Models ; using System . Linq ; us i ng SportsStore . Models . ViewMode l s ; namespace SportsStore . Co ntrolle r s ( puЫic class ProductController : Contro l ler private IProductRepository repo s itory ; puЫic int PageSize = 4 ; puЫic ProductControl l er(IProductRep ository repo) { repository = repo ; puЫic ViewResult List(string category, int page = 1) => View(new Prod uctsListVi e wMo de l { Products = repository . Products . Where (р => са tegory == null 1 1 р. Са tegory == са tegory) . OrderBy(p => p . Produc t ID) . Skip( (page - 1) * PageSi ze) . Take ( PageSize) , Paginginfo = new Pagingi nfo Current Page = page, ItemsPerPage = PageSize , Total i t ems = reposit ory. Products. Count() }' CurrentCategory = category }); В этот метод действия вн есены три изменения . Первое - добавлен новый пара­ метр по им е ни category. Он применяет ся вторым изменением , которое представ­ ляет собой расширение з апроса LINQ. Если значение category не равно null , тогд а выбираются только объекты P r o du ct с соответствующим значени е м в свойс­ тве Category. Последнее, третье , изменение касается устан овки значения свойс­ тв а Cur r entCategory, которое было добавлено в класс ProductsListViewModel . Однако в результате таких изменений значение Paginglnfo . Totalltems вычи сля ­ ется некорректно , потому что оно не принимает во внимани е фильтр по категории. Со временем мы все исправим . Модульное тестирование: обновление существующих модульных тестов Мы изменили сигнатуру метода действия List () , поэтому некоторые существующие ме­ тоды модульного тестирования перестали компилироваться . Чтобы решить возникшую про­ блему, в модульных тестах, которые работают с контроллером, методу действия Li s t ()
246 Часть 1. Введение в инфраструктуру ASP.NET Core MVC понадобится передавать в первом параметре значение null. Например, раздел действия тестового метода Can_ Paginate () в файле ProductControllerTests . cs дол же н выглядеть следующим образом : [Fact] puЫic void Can_Paginate() 11 Организация Mock<IProductRepository> mock = new Mock<IProductRepository>() ; mock.Setup(m => m.Products) .Returns(new Product[] new Product {ProductID = 1, Name Pl} , new Product {ProductID 2, Name Р2} , new Product {ProductID 3 , Name РЗ}, new Product {ProductID 4 , Name Р4} , new Product {ProductID 5 , Name Р5) }); ProductController controller controller . PageSize = 3 ; 11 Действие ProductsListViewModel result new ProductController(mock . Object) ; controller.List(null, 2) .ViewData.Model as ProductsListViewModel; 11 Утверждение Product[J prodArray = result . Products .T oArray(); Assert.True(prodArray.Length == 2) ; Assert . Equal(P4 , prodArray[O ] .Name); Assert.Equal(P5 , prodArray[l] .Name) ; Указывая null для category, мы получаем все объекты Product, которые контрол­ лер извлекает из хранилища, что воспроизводит ситуацию перед добавлением ново ­ го параметра. Такого же рода изменение необходимо внести также в тестовый метод Can_Send_Pagination_View_Model(): [ Fact] p u Ыic void Can Send Pagination View Model() { 11 Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock . Setup(m => m.Products) .Returns(new Product[ ] { new Product {ProductID 1 , Name Pl} , new Product {ProductID 2 , Name Р2}, new Product {ProductID З , Name РЗ} , new Product {ProductID 4 , Name Р4} , new Product {ProductID 5 , Name Р5} }); 11 Организация ProductController controller = new ProductController(mock . Object) { PageSize 3 }; 11 Действие ProductsListViewModel result = controller.List(null, 2) .ViewData.Model as ProductsListViewModel;
11 Утверждение Paginginfo pageinfo = resu lt. Paging l nfo ; Assert . Equal(2, pageinfo . CurrentPage) ; Assert.Equal(З , pageinfo.ItemsPerPage) ; Assert . Equal(5 , pageinfo .T otalitems ) ; Assert . Equal(2 , pageinfo .T ota l Pages) ; Глава 9. SportsStore: навигация 247 Когда вы примете образ мышления, ориентированный на тестирование, поддержание мо­ дульных тестов в синхронизированном состоянии с изменениями кода быстро станет вашей второй натурой. Чтобы увидеть результат фильтрации по категории , запустите приложение и вы­ берите категорию с помощью показанной ниже строки запроса, заменив номер порта тем, который был назначен вашему проекту средой Visual Studio (позаботившись о том, что Soccer начинается с прописной буквы S): http://localhost : 60000/?category=Soccer Вы увидите только товары из категории Soccer (рис. 9 . 1) . Очевидно, что пользователи не захотят переходить по категориям с прим енением URL , но здесь было показано, что незначительные изменения в приложении МVС мо­ гут оказывать крупное влияние, если базовая структура на месте. Put something us eful here later Soccer Ball FIFA-approved size and VJeight Corner Flags Give your playing field а professlonal touch Stadium Flat-packed 35,000 -seat stadium 11" j Рис. 9.1. Использование строки запроса для фильтрации по категории
248 Часть 1. Введение в инфраструктуру ASP.NET Core MV C Модульное тестирование: фильтрациs~ по категории ------- Нам необходим модульный тес т для проверки функциональности фильтрации по кате­ гории, чтобы удостовериться в том , что фильтр может корректно генерировать сведения о товарах указ анной категории. Ниже приведен тестовый метод, добавленный в класс ProductControllerTests: [Fact] puЬlic void Can Filter Products() { 11 Ор ган изация - создание имитирова н ного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>() ; mock . Setup(m => m.Products) .Returns(new Product[] { new Product {ProductID 1 , Name Pl , Catego r y = Catl} , new Produ ct {ProductID 2 , Name Р2, Category Cat2} , new Product {Prod uctID 3 , Na me РЗ, Category = Catl}, new Product {ProductID 4, Name Р4 , Category = Cat2} , new Product {ProductID 5 , Name Р5 , Category = СаtЗ} }); 11 Организация - создание контроллера и установка размера // страницы в три элемента ProductController controller = new ProductController(mock . Ob ject} ; controller . PageSize = З ; 11 Действие Product[ ] resu l t = (control l er . List(Cat2, 1) . ViewData .M odel as ProductsListViewModel) . Products.ToArray(); //Утверждение Assert . Equal(2 , result.Length) ; Assert . True(result[O] . Name Р2 && result[O] . Category Assert . True(result[l ] . Name == Р4 && result[l] . Category Cat2); Cat2); Этот тест создает имитированное хранилище, содержащее объекты Product , которые от­ носятся к диапазону категорий . С использованием метода действия List () запрашивается одна специфическая категория, а результаты проверяются на предмет наличия корректных объектов в пр авильном поряд ке. Улучшение схемы URL Мало кому понравится видеть либо иметь дело с неуклюжими URL вроде /?category=Soccer . Для реш ения данной проблемы мы намерены изменить схему маршрутизации в методе Configure () класса Startup, чтобы создать более удоб­ ный набор URL (листинг 9.3) . Внимание ! Новые маршруты в листинге 9 .3 ва ж но добавлять в показанном порядке . Маршруты при меняются в поряд ке, в котором они определены, поэтому изменение по ­ рядка может привести к нежелательным эффектам.
Глава 9 . SportsStore : навигация 249 Листинг 9.3 . Изменение схемы маршрутизации в файле Startup. cs puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment env , IL ogger Fa ctory l oggerFactory) { app.Use De ve l operExceptionPag e() ; app . UseStatusCodePages() ; app.UseStaticFiles() ; app . UseMvc(routes = > { routes .МapRoute ( name: null, template: {category}/Page{page:int}, defaults: new { controller = Product, action = List } ); routes.MapRoute( name: null, template: Page{page:int}, defaul ts: new { controller = Product, action = List, page = 1 } ); routes.МapRoute( name: null, template: {category}, defaults: new { controller = Product, action = List, page = 1 } ); routes .MapRoute ( name: null, template: , defaul.ts : new { controller = Product, action = List, page = 1 } ); routes.МapRoute(name: null, template: {controller}/{action}/{id?}); )); SeedData . EnsurePopu l ated(app) ; В табл. 9 . 1 описана схема URL, которую представляют эти маршруты. Система маршрутизации подробно объясняется в главах 15 и 16. Таблица 9.1. Сводка по маршрутам URL / /Page2 /Soccer /Soccer/Page2 Что делает Выводит первую стран и цу списка товаров всех категорий Выводит указанную страницу (в данном случае страницу 2 ), отображая товары всех категорий Выводит первую страницу товаров указанной категории (Soccer) Выводит указанную страницу (страницу 2 ) товаров заданной категории (Soccer)
250 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Система маршрутизации ASP.NET используется инфраструктурой MVC для об­ работки входящих запросов от пользователей, но также генерирует исходящие URL, которые соответствуют схеме URL и потому могут встраиваться в веб-страницы. Применение системы маршрутизации для обработки входящих запросов и генерации исходящих URL позволяет гарантировать согласованность всех URL в приложении. Интерфейс IUrlHelper предоставляет доступ к функциональности генерации URL. Мы использовали этот интерфейс и определяемый им метод Action () в де­ скрипторном вспомогательном классе, созданном в предыдущей главе. Теперь. когда нужно генерировать более сложные URL, необходим способ получения дополнитель­ ной информации от представления. не добавляя дополнительные свойства к дескрип­ торному вспомогательному классу. К счастью, дескрипторные вспомогательные клас­ сы обладают удобной возможностью, которая позволяет получать в одной коллекции все свойства с общим префиксом (листинг 9.4). Листинг 9.4. Получение значений атрибутов, снабженных префиксом, в файле PageLinkTagHelper. cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using SportsStore.Models.ViewModels; using System.Collections.Generic; namespace SportsStore.Infrastructure [HtmlTargetElement(div, Attributes = page-model)] puЫic class PageLinkTagHelper : TagHelper { private IUrlHelperFactory urlHelperFactory; puЫic PageLinkTagHelper(IUrlHelperFactory helperFactory) urlHelperFactory = helperFactory; [ViewContext] [HtmlAttributeNotBound] puЫic ViewContext ViewContext { get; set; puЫic Paginginfo PageModel { get; set; puЫic string PageAction { get; set; ) [HtmlAttributeName(DictionaryAttributePrefix = page-url-)] puЬlic Dictionary<string, object> PageUrlValues { get; set; = new Dictionary<string, object>(); puЫic bool PageClassesEnaЫed { get; set; } = false; puЬlic string PageClass { get; set; } puЫic string PageClassNormal { get; set; } puЬlic string PageClassSelected { get; set; puЬlic override void Process(TagHelperContext context, TagHelperOutput output) { IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext); TagBuilder result = new TagBuilder(div); for (int i = l; i <= PageModel.TotalPages; i++) { TagBuilder tag = new TagBuilder(a);
Глава 9. SportsStore: навигация 251 PageUrlValues[page] = i; tag.Attributes[href] = urlHelper.Action(PageAction, PageUrlValues); i f ( PageClas sesEnaЬled) { tag . AddCssClass(PageClass); tag . AddCssClass(i == PageModel.CurrentPage ? PageCl assSe l ected : PageClassNormal); tag.InnerHtml . Append(i .T oString()); result .InnerHtml . AppendHtml(tag); output . Content . AppendHtml(result .I nnerHtml); Декорирование свойства дескрипторного вспомогательного класса посредс­ твом атрибута HtmlAttributeName позволяет указывать префикс для имен атри­ бутов элемента, которым в данном случае будет page - url-. Значение любого ат­ рибута, имя которого начинается с такого префикса, будет добавлено в словарь, присваиваемый свойству Р age Ur 1Vа1 ue s . Затем это свойство передается методу IUrlHelper . Action () с целью генерации URL для атрибута href элементов а , ко­ торые производит дескрипторный вспомогательный класс. В листинге 9.5 к элементу d i v добавлен новый атрибут, который обрабатывается дескрипторным вспомогательным классом и указывает категорию, применяемую для генерации URL. В представление был добавлен только один атрибут, но в словарь до­ бавился бы любой атрибут с тем же самым префиксом. Листинг 9.5 . Добавление нового атрибута в файле List. cshtml @mode l ProductsListViewModel @foreach (var р in Mode l. Products) @Html . Partial(ProductSummary , р) <div page-mode l =@Mode l.Paginginfo page - action=List page-clas ses-enaЫed=true page-class=btn page - class -no rmal=btn - defau lt page-class-selected=btn-primarypage-url-category=@Model.CurrentCategory class=btn -group pu ll- right> </div> До внесения этого изменения ссылки для перехода на страницы генерировались так: http : //<cepвep> : <пopт>/Page l Если пользователь щелкнет на страничной ссылке подобного типа, то фильтр по категории утрачивается и приложение отобразит страницу, содержащую товары всех категорий. За счет добавления текущей категории, получаемой из модели представления, вза­ мен генерируются URL следующего вида: http : //<cepвep> : <пopт>/Chess/Pagel
252 Часть 1. Введение в инфраструктуру ASP. NEТ Соге MVC Когда пользователь щелкает на ссьmке такого рода, текущая категория передается методу действия List () и фильтрация сохраняется. После внесения данного измене­ ния можно посещать URL вроде /Chess или /Soccer и наблюдать, что страничные ссылки, расположенные внизу, корре1пно включают категорию. Построение меню навигации по категориям Нам необходимо предложить пользователям способ выбора категории, который не предусматривает ввод URL. Это означает, что мы должны отобразить список доступ­ ных категорий с отмеченной текущей 1<атегорией, если она есть. По мере построения приложения список категорий будет задействован в более чем одном контроллере, по­ этому он должен быть самодостаточным и многократно используемым. В инфраструктуре ASP.NET Core MVC поддерживается концепция компонентов представлений, которые идеально подходят для создания единиц вроде много1<ратно используемого навигационного элемента управления. Компонент представления - это 1шасс С#, который предоставляет небольшой объем многократно используемой приrтадной логики с возможностью выбора и отображения частичных представле­ ний Razor. Компоненты представлений подробно рассматриваются в главе 22. В данном случае мы создадим компонент представления, который визуализиру­ ет навигационное меню и интегрирует его в приложение за счет обращения к этому компоненту из разделяемой компоновки. Такой подход дает нам обычный класс С#, который может содержать любую необходимую прикладную логику и который можно подвергать модульному тестированию подобно любому другому классу. Это удобный способ создания небольших сегментов приложения, сохраняя общий подход МVС. Создание навигационного компонента представления Создайте папку по имени Cornponents , которая по соглашению является местом хранения компонентов представлений, и добавьте в нее файл класса под названием NavigationMenuViewCornponent. cs с определением, показанным в листинге 9.6. Листинг 9.6. Содержимое файла NavigationМenuViewComponen t. cs из папки Components using Microsoft.AspNetCore . Mvc ; namespace SportsStore . Components puЫic class NavigationMenuViewComponent : ViewComponent puЫic string Invoke() { re tu rn Hello from the Nav View Component ; Метод Invoke () компонента представления вызывается, когда компонент приме­ няется в представлении Razor. а результат, возвращаемый методом Invoke (), встав­ ляется в НТМL-разметку, отправляемую браузеру. Мы начали с простого компонента представления, который возвращает строку, но вскоре заменим его динамическим НТМL-содержимым. Список категорий должен присутствовать на всех страницах, поэтому мы собира­ емся использовать компонент пр едставления в разделяемой компоновке. а не в отде-
Глава 9. SportsStore: навигация 253 льном представлении. Внутри компоновки компоненты представлений применяются через выражение @awai t Component . InvokeAsync (),как показано в листинге 9.7 . Листинг 9.7. Использование компонента представления в файле _Layout.cshtml < ! DOCTYP E html> <html> <head> <meta name=viewport content=wi dth=device-width /> <l ink r el=styleshe et asp -hr ef - include=liЬ/bootstrap/dist/css/* . min . css /> <title>SportsStore</title> </head> <body> <div class=navbar navbar-inverse role=navigation> <а class =navbar -brand href=#>SPORTS STORE</a> </div> <div class=row panel> <div id=categories clas s=col -x s-3> @await Component.InvokeAsync(NavigationМenu) </div> <div class=col - xs -8> @Render Body () </div> </div> </body> </html> Текст заполнителя заменен вызовом метода Component . InvokeAsync () . Аргументом этого метода является имя класса компонента без части ViewComponent, т.е. с помощью NavigationMenu указывается класс NavigationMenuViewComponent . Запустив приложение, вы увидите, что вывод из метода InvokeAsync () включен в НТМL-разметку, отправляемую браузеру (рис. 9.2). SPORTS STORE 1 Hello from the NavView Component Kayak А boat fог one person lfifiфl Рис . 9.2 . Применение компонента представления
254 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Генерация списков категорий Теперь можно возвратиться к навигационному компоненту представления и сгене­ рировать реальный набор категорий. Построить НТМL-разметку для категорий можно было бы вручную , как делалось в дескрипторном вспомогательном классе для стра­ ничных ссылок, но одно из преимуществ работы с компонентами представлений за­ ключается в том, что они могут визуализировать частичные представления Razor. Это означает, что компонент представления можно использовать для генерации списка категорий и затем применить более выразительный синтаксис Razor для визуализа­ ции НТМL-разметки, которая отобразит данный список. Первым делом нужно обно­ вить компонент представления, как показано в листинге 9.8. Листинг 9.8 . Добавление списка категорий в файле NavigationМenuViewComponent.cs using Microsoft . AspNetCore . Mvc ; using System.Linq; using SportsStore.Models; namespace SportsStore . Components puЫic class NavigationMenuViewComponent : ViewComponent { private IProductRepository repository; puЫic NavigationМenuViewComponent(IProductRepository repo) repository = repo; puЫic IViewComponentResul t Invoke () return View(repository . Products .Select(x => x.Category) . D istinct () . OrderBy(x => х)); Конструктор, определенный в листинге 9.8, принимает аргумент типа IProductReposi t ory . Когда инфраструктуре MVC необходимо создать экземпляр класса компонента представления, она отметит потребность в предоставленИи это­ го аргумента и просмотрит конфигурацию в классе Startup, чтобы выяснить, какой объект реализации должен использоваться . Мы имеем дело с тем же самым ср едством внедрения зависимостей, которое применялось в контроллере из главы 8, и результат будет аналогичным - предоставление компоненту представления доступа к данным без необходимости знать то, какая реализация хранилища будет использоваться. как описано в главе 18 . В методе Invoke () с помощью LINQ выбирается и упорядочивается набор катего­ рий в хранилище, после чего он передается в качестве аргумента методу View (),к о­ торый визуализирует стандартное частичное представление Razor. Детали этого час­ тичного представления возвращаются из метода с применением объекта реализации IViewComponentResult (данный процесс будет подробно рассмотрен в главе 22).
Глава 9. SportsStore: навигация 255 Модульное тестирование: генерация списка категорий Модульный тест, предназначенный для проверки возможности генерации списка категорий, относительно прост. Цель заключается в создании списка, который отсортирован в алфа­ витном порядке и не содержит дубликатов. Проще всего это сделать, построив тестовые данные , которые имеют дублированные категории и не отсортированы должным образом, передав их дескрипторному вспомогательному классу и установив утверждение, что данные были соответствующим образом очищены . Вот модульный тест, который определяется в но­ вом файле класса по имени Na vig ationMenuViewC omponen t Te sts. cs внутри проекта SportsStor e .Tests : using Sys t em . Collections . Generic ; using System . Linq ; using Microsoft.AspNetCo r e . Mvc . ViewCompon e nts ; using Moq ; using Spor t sStore . Components ; usi ng SportsStore.Models; using Xuni t ; namespace SportsStore . Te st s puЬlic class Navigation MenuV i ewCompon entTests [Fact] puЫic void Сап Sele ct Categories () ( 11 Орган иза ция Mock< I ProductRepos i tor y> mock = n ew Mock<IProductRep os i to r y>() ; mock . Setup(m => m . Prod u cts) .Returns(new Product[ ] { new P r oduct {Produ ctID 1 , Name Pl , Category Apples }, new Product {Product I D 2 , Name Р2 , Category Apples} , new Product {Product ID new Product (Pr oductID )); 3, Name 4, Name РЗ , Category Р4 , Category NavigationMenuViewComponent target = new Navi gationMenuVi ewComponent(mock . Object) ; 11 Действие - п олучени е набора кате г орий Pl ums} , Orang es} , string [ ] resul .ts = ( (IEnumeraЫe< string>) (target. Invoke () as Vi ewViewComponent Result) .Vi ewDat a . Model) .ToAr ray() ; 11 Утверждение Asse rt. True(EnumeraЫe . Sequence Equ al(new s t ri n g [] { App l es , Oranges, Plums }, results)); Мы создаем имитированную реализацию хранилища, которая содержит повторяющиеся и несортированные категории. Затем мы устанавливаем утверждение о том , что дубликаты удалены и восстановлен алфавитный порядок. Создание представления Как будет объясняться в главе 22, при работе с представлениями, которые выби ­ раются компонентами представлений , механизм Razor использует разнообразные со­ гл аш е ния . И стандартное имя представл е ния, и м естоположения , в которых ищется
256 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC представление, отличаются от тех, которые приняты для контроллеров. Создайте пап­ ку Views/Shared/Components/NavigationMenu и добавьте в нее файл представле­ ния по имени Default . cshtml с содержимым, приведенным в листинге 9.9. Листинг 9.9. Содержимое файла Defaul t. cshtml из папки Views/Shared/Components/NavigationМenu @model IEnumeraЫe<string> <а c lass=btn btn-Ьlock btn-default asp-action=List asp - co ntroller=Pr o duct asp-route - category=> Home </а> @foreach (stri ng category in Model) { <а class=btn btn-Ыock btn-default asp-a ction=List asp-controller=Product asp -route - category=@category asp -ro ute-page=l > @category </а> В представлении применяется один из встроенных дескрипторных вспомогатель­ ных классов (описанных . в главах 24 и 25) для создания элементов а, атрибут href которых содержит URL, выбирающий определенную категорию товаров. Запустив приложение, вы увидите ссылки на категории (рис. 9.3). Щелчок на ка­ кой-то категории приводит к тому, что список товаров обновляется, отображая только товары выбранной категории. CJ Spo1tsStor~ Х ~ ".~ С 1<D lo ca ll10st :60000/Soccer/Pa9e1 t _____________, SРОПП.~ SЮПЕ Hom e Chess Soccer Ball Socce r FIFA -approved size and weight Wa! erspo rt s 1 1 1 1 1 1 1 1 .; ...J Рис. 9.3 . Генерация ссылок на категории с помощью компонента представления
Глава 9. SportsStore: навигация 257 подсветка текущей категории В настоящий момент пользователь не располагает накой-нибудь визуальной под­ СI<азной о выбранной натегории. Хотя, основываясь на товарах в списке, можно вы­ двинуть предположение относительно 1<атегории, гораздо лучше предоставить более наглядную визуальную обратную связь. Компоненты ASP.NET Core MVC, такие как контроллеры и компоненты представлений, могут получать информацию о текущем запросе, обращаясь к объекту контекста. Большую часть времени заботу о получении объекта контекста можно поручить базовым классам, которые используются для со­ здания компонентов, подобно тому, как базовый класс Controller применяется для создания контроллеров. Базовый класс ViewComponent не является исключением и обеспечивает доступ к объектам контекста через набор свойств. Одно из свойств называется RouteData и предоставляет информацию о том, как URL запроса был обработан системой маршрутизации. В листинге 9 .1 О свойство Rou t е Da t а используется для доступа к данным запро­ са, чтобы получить значение выбранной в тенущий момент категории. Значение ка­ тегории можно было бы передать представлению путем создания еще одного класса модели представления (и так бы делалось в реальном проенте), но ради разнообразия применим объект ViewBag, ноторый был введен в главе 2. Листинг 9.1 О. Передача выбранной категории в файле NavigationМenuViewComponent.cs using Microsoft.AspNetCore.Mvc ; using System.Linq; using SportsStore.Models ; namespace SportsStore . Components puЫic class NavigationMenuViewComponent : ViewComponent { private IProductRepository repository; puЬlic NavigationMenuViewComponent(IProductRepository repo) repository = repo ; puЫic IViewComponentResult Invoke() { ViewBag.SelectedCategory = RouteData?.Values[category]; return View(repository .Pr oducts . Select(x => x .Category) . Distinct () . OrderBy(x => х)); Внутри метода Invoke () мы динамичесни создаем свойство SelectedCategory в объекте ViewBag и устанавливаем его значение равным значению текущей катего­ рии, которое получаем через объент контекста, возвращенный свойством RouteData. Как объяснялось в главе 2, ViewBag представляет собой динамический объент, 1юто­ рый позволяет определять новые свойства, просто присваивая им значения.
258 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Модульное тестирование: сообщение о выбранной категории Для выполнения проверки того , что компонент представления корректно добавил детали о выбранной категории, в модульном тесте можно прочитать значение свойства ViewBag, которое доступно через класс ViewViewComponentResult, описанный в главе 22 . Ниже показан модульный тест, добавленный в класс NavigatioMenuViewComponentTests: [Fact] puЬlic void Indicates Selected Category() 11 Организация string categoryToSelect = App les; Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock . Setup(m => m . Products) . Returns(new Product(] { new Product {ProductID 1 , Name Pl , Category Apples} , new Product {ProductID = 4 , Name = Р2 , Category = Oranges} , }); NavigationMenuViewComponent targe t = new NavigationMenuViewComponent(mock . Ob j ect) ; target . Vi ewComponentContext = new ViewComponentContext ViewConte xt = new ViewContext { } }; RouteData = new RouteData() target .RouteData . Values(category] = categoryToSelect; 11 Действие string result = (string) (target.Invoke() as ViewViewComponentResult) . ViewData(SelectedCategory] ; 11 Утверждение Assert .Equal(categoryToSe le ct , result}; Этот модульный тест снабжает компонент представления данными маршрутизации через свойство ViewComponentContext , посредством которого компоненты представлений получают все свои данные контекста. Свойство ViewComponentContext предоставляет доступ к данным контекста, специфичным для представления, с помощью своего свойства ViewContext , которое, в свою очередь, обеспечивает доступ к информации о маршрути­ зации через свое свойство RouteData . Большая часть кода в модульном тесте связана с созданием объектов контекста, которые будут предоставлять выбранную категорию таким же способом, как она бы предлагалась во время выполнения приложения, когда данные кон­ текста предоставляются инфраструктурой ASP.NET Саге MVC. Теперь , когда доступна информация о том, какая категория выбрана, можно обно­ вить пр едставление, выбираемое компонентом представления, чтобы задействовать эту информацию, и изм енить классы CSS, которые используются для стили зации ссылок, сделав представление текущей категории отличающимся от остальных кате­ горий . В листинге 9 .11 приведено изменение, внесенное в файл Defaul t . cshtml.
Глава 9. SportsStore: навигация 259 Листинг 9.11. Подсветка текущей категории в файле Defaul t. cshtml @model IEnumeraЫe< st ring> <а class=btn btn -Ьloc k btn- defa ul t asp- act ion=List asp - control ler= Pr oduct asp- route- categor y=> Ноте </а> @foreach (st ring cat egory in Model) { <а class=btn Ьtn-Ыock @(category == ViewBag.SelectedCategory? Ьtn-primary: Ьtn-default) asp- acti on=List asp -con t roller=P rod u c t asp - route - categor y=@cate gory asp- route- page=l> @category </а> С помощью выражения Razor внутри атрибута c lass мы применяем класс Ьtn - pr irnary к элементу , который представляет выбранную категорию , и класс Ьtn - default к остальным элементам . Указанные классы применяют разные стили Boots trap и делают активную кнопку визуально отличающейся (рис. 9.4). D Sport1Stort Х SPOf1fS SIORE Home Socce r Waterspol1s Thinking Сар lmprove brain ef!lclency Ьу 75% 1 ~Ch~--"'-~~j Рис. 9.4 . Подсвечивание выбранной категории Корректировка счетчика страниц Мы должны скорректировать ссылки на страницы , чтобы они правильно работали , когда выбрана какая-то категория. В настоящий момент количество ссьmо1< на стра­ ницы определя ется общим числом товаров в хранилище, а не количеством товаров выбранной категории. Это значит, что пользователь может щелкнуть на ссылке для страницы 2 категории Chess и получить пустую страницу, поскольку товаров данной категории не хватает для заполнения второй страницы. Проблема демонстрируется на рис. 9.5.
260 Часть 1. Введение в инфраструктуру ASP.NET Core MVC IJ SportsStore Х f- С ~.;;;;IOOO/Chess/P•g".2 SРОПТS STOHE: Home Chess • ' Socce r Waler~ports 1.3 1 1 i _:_ _ __- - -'1 Рис. 9.5. Отображение некорректных ссылок на страницы, когда выбрана какая-то категория Проблему можно устранить, модифицировав метод действия List () в контролле­ ре Product так, чтобы категории принимались во внимание при разбиении на стра­ ницы (листинг 9. 12). Листинг 9.12. Создание данных о разбиении на страницы, учитывающих категории, в файле ProductController. cs using Microsoft.AspNetCore . Mvc ; using SportsStore . Models ; using System . Linq ; using SportsStore.Models.ViewMode l s ; namespace SportsStore . Controllers { puЬlic class ProductController : Controller private IProductRepository repository ; puЬlic int PageSize = 4 ; puЫic ProductController(IProductRepository repo) { repository = repo; puЬlic ViewRes ult List(string category , int page 1) => View(new ProductsListViewModel Products = repository .Products . Where(p => category == null 11 p . Category category) . OrderBy(p => p .ProductID) . Skip( (page - 1) * PageSize) . Take(PageSize) , Paginginf o = new Paginginf o CurrentPage = page , ItemsPerPage = PageSize , Totalitems = category == null ? }' repository.Products.Count() repository.Products.Where(e => e.Category == category) .Count() CurrentCategory = category });
Глава 9. SportsStore: навигация 261 Если категория была выбрана, тогда возвращается количество позиций в ней, а если нет, то общее число товаров. Теперь во время просмотра товаров в какой-либо категории ссылки в нижней части страницы корректно отражают количество товаров в этой категории (р ис. 9.6). С) Spott~S 1ort Х r: -~- ---· --- ----·----·-- -··-··-------] J ~ • С l-~~~~lh~:_~:::_~c!'.':~~"2.:~--- ----------·-----~- Sf 'Olrl"!-> ~ТОНЕ Нота Тhinking Сар Soccor lmprove brain effi ciency Ьу 75% Wato rsports Рис. 9.6. Отображение ссылок на страницы с учетом выбранной категории Модульное тестирование: счетчик товаров определенной категории Протестировать возможность генерации корректных счетчиков товаров для различных категорий очень просто. Мы создадим имитированное хранилище, которое содержит из­ вестные данные в определенном диапазоне категорий, и затем вызовем метод действия List (), запрашивая каждую категорию по очереди. Вот модульный тест, добавленный в класс ProductControllerTests : [Fact] puЬlic void Generate_Category_Specific_Product_ Count() { 11 Организация Mock<IProd uctRep os itory> mock = new Mock<IProductRepository>() ; mock . Setup(m => m.Products) .Returns(new Product[] ( new Product {ProductID 1, Name Pl , Category Catl}, new Product {ProductID 2 , Name Р2 , Category = Cat2}, new Product {ProductID 3 , Name РЗ, Category = Catl}, new Product {ProductID 4 , Name Р4, Category = Cat2}, new Product {ProductID = 5 , Name PS , Category = СаtЗ) }); ProductController target = new ProductController(mock.Object); target.PageSize = 3 ; Func<ViewResult, ProductsListViewModel> GetModel = result => result?.ViewData?.Model as ProductsListViewModel ;
262 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 11 Действие int? resl GetModel(target .Li st(Catl) )?.Paginginfo.Totalitems; int? res2 = GetModel(target .List(Cat2) )?.Paginginfo.Totalitems; int? resЗ = GetModel(target.List(CatЗ) )?.Paginginfo.Totalitems ; int? resAll = GetModel(target.List(null))? . Paginginfo.Totalitems; 11 Утверждение Assert.Equal(2 , resl); Assert.Equal(2, res2) ; Assert .Equal(l, resЗ); Assert .Equal(5 , resAll); Обратите внимание, что в модульном тесте также вызывается метод Lis t () без указания категории, чтобы удостовериться в правильности подсчета общего количества товаров . Построение корзины для покупок Приложение продолжает расширяться, но из-за того, что корзина для покупок пока не реализована, продавать товары невозможно. В настоящем разделе мы созда­ дим корзину для покупок согласно иллюстрации на рис. 9.7 . Если вы приобретали что-нибудь в электронных магазинах, то она должна выглядеть знакомой. Товары Футбольный мяч 1Добавить в корзину 1 Угловые флажки 1Добавить в корзину 1 Стадион Добавить в корзину Ваша корзина 1 х Стадион $79,500.00 Всего $79,500.00 Перейти к оплате ...i i(-----11Продолжить покупку 1 Рис. 9. 7 . Базовый поток корзины для покупок Ввод информации о доставке ". ит.д". Кнопка добавления в корзину (Add to cart) будет отображаться рядом с каждым товаром в каталоге. Щелчок на ней будет приводить к выводу сводки по товарам, которые уже были выбраны пользователем, включая общую стоимость. В этой точ­ ке пользователь может с помощью кнопки продолжения покупки (Continue shopping) возвратиться в каталог товаров, а посредством кнопки перехода к оплате (Checkout now) - сформировать заказ и завершить сеанс покупки. Определение модели корзины Начните с добавления в папку Models файла класса по имени Cart . cs с опреде­ лениями, показанными в листинге 9.13. Листинг 9.13. Содержимое файла Cart. cs из папки Models using System . Collections . Generic ; using System . Linq;
narnespace SportsStore . Models { puЫic class Cart { Глава 9. Spo rtsStore : нав ига ция 263 private List<CartLine> lineCollection = new List<CartLine>(); puЫic virtual void Additern(Product product , int quantity) CartLine line = lineCollection . Where(p => p.Product . ProductID == product.ProductID) .FirstOrDefault() ; if (line == null) { lineCollection . Add(new CartLine { Product = product , Quantity = quantity }); else { line.Quantity += quantity ; puЫic virtual void RernoveLine(Product product) => lineCollection.RernoveAll(l => l . Product . ProductID == product.ProductID); puЫic virtual decirnal CornputeTotalValue() => lineCollection . Surn(e => e . Product . Price * e . Quantity) ; puЫic virtual void Clear() => lineCollection . Clear() ; puЫic virtual IEnurneraЫe<CartLine> Lines => lineCollection; puЫic class CartLine { puЫic int CartLineID { get; set; } puЫic Product Product { get; set; } puЬlic int Quantity { get ; set; } Класс Cart использует класс CartLine, который определен в том же самом файле и представляет товар, выбранный пользователем, а также приобретаемое его коли­ чество. Мы определили методы для добавления элемента в корзину, удаления элемен ­ та из корзины, вычисления общей стоимости элементов в корзине и очистки ко рзины путем удаления всех элементов. Мы также предоставили свойство, которое позволяет обратиться к содержимому корзины с применением IEnumeraЬle<CartLine>. Вс е это легко реализуется с помощью кода С# и небольшой доли кода LINQ. М одульное тестирование: проверка корзины Класс Cart относительно прост, но в нем присутствует ряд важны х линий поведения , кото­ рые должны корректно работать. Неправильно фун к ционирующая корзина нарушит работу всего приложения SportsStore . Мы разобьем средства на части и протестируем их по отде ­ льности. Для раз ме щения тестов в проекте SportsStore . Tests создается новый файл по имени CartTests . cs . Первая линия поведения относится к добавлению элемента в корзину. При первоначаль­ ном добавлении в корзину объекта Product должен быть добавлен новый экземпляр CartLine. Ни же представлен тестовый метод вместе с определением класса модульного тестирования.
264 Часть 1. Введение в инфрастру к туру ASP. NEТ Core MVC using System . Linq ; using SportsStore.Models ; using Xunit ; namespace SportsStore . Tests puЬl i c class CartTests { [Fact] puЫic void Can_Add_New_Lines() { 11 Организация - создание нескольких тестовых товаров Product pl = new Product { ProductID = 1, Name Pl }; Product р2 = new Product { ProductID = 2 , Name = Р2 }; 11 Организация - создание новой корзины Cart target = new Cart() ; 11 Действие target . Additem(pl , 1) ; target . Additem(p2 , 1) ; CartLine[] resu l ts = target .Lines.ToAr r ay() ; 11 Утверждение Assert . Equal(2 , results . Length) ; Assert . Equal(pl , results[O] .Product ); Assert . Equal(p2 , results[l] .Prod u ct) ; Но если пользователь уже добавлял объе кт Produ ct в корзину, тогда необходимо уве­ личить количество в соответствующем экземпляре CartLine , а не создавать новый . Вот модульный тест: [Fact] puЫic void Can Add Quantity For Existing Lines() { 11 Организация - создание нескольких тестовых товаров Product pl = new Product { ProductID = 1, Name Pl }; Product р2 = new Product { ProductID = 2 , Name = Р2 }; 11 Организация - создание новой корзины Cart target = new Cart() ; 11 Действие target.Additem(pl , 1 ) ; target . Additem(p2 , 1) ; target.Add i tem(pl , 10) ; CartLine [] res ults = target .Lines . OrderBy(c => c.Product.ProductI D) .T oA r ray() ; 11 Утверждение Assert . Equal(2 , results.Length); Assert . Equal(ll , results[O] .Quantity) ; Assert . Equal(l , results[l ] . Quantity) ; Нам также необходимо проверить, что пользователи имеют возможность менять свое решение и удалять товары из корзины. Эта линия поведения реализуется методом RemoveLine () . Модульный тест выглядит следующим образом :
Глава 9. SportsStore: навигация 265 [Fact] puЫic void Can _ Remove Line () { //Организация - создание нескольких тестовых товаров Product pl new Product { ProductID 1 , Name Pl ) ; Product р2 = new Product { ProductID= 2, Name Р2 ); Product рЗ = new Product { ProductID = 3 , Name РЗ ) ; //Организация - создание новой корзины Cart target = new Cart() ; // Организация - добав лени е некоторых товаров в корзину target . Additem(pl , 1) ; target . Additem(p2 , 3) ; target .Additem(pЗ , 5) ; target . Additem(p2 , 1) ; 11 Действие target . RemoveLine(p2) ; 11 Утверждение Assert . Equal(O, target . Li nes . Where{c = > c . Product Assert . Equal(2 , target . Lines . Count()) ; р2) .Count() ); Далее проверяется лин ия поведения , связанная с возможностью вычисления общей стои­ м ости элементов в корзине . Вот модульный тест: [Fact] puЫic void Calculate_Cart_ Total() { 11 Организация - создание нескольких тест овых товаров Product pl = new Product { ProductID = 1, Name Pl , Pri ce Product р2 = new Produc t { ProductID = 2 , Name = Р2 , Price / / Организация - создание новой корзины Cart target = new Cart(); 11 Действие target.Additem(pl , 1); target.Additem(p2 , 1); target.Additem(pl , 3) ; decimal result = target . ComputeTot a l Va lue () ; 11 Утверждение Assert.Equa l (450M , result) ; lOOM ); 50М ); Последний тест очень прост. Мы дол ж ны удостовериться , что в результате оч и ст ки корзины ее содержимое корректно удаляется. Ниже показан модульный тест. [Fact ] puЫic void Can_ Clear Contents() { 11 Организация - создание нескольких тест овых товаров Product pl new Product { ProductID 1 , Name Pl , Pri ce Product р2 = new Product { ProductI D = 2 , Name = Р2 , Price lOOM }; 50М );
266 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC //Организация - создание новой корзины Cart target = new Cart() ; 11 Организация - добавление нескольких элементов в корзину ta r get . Additem(p l, 1); target . Additem(p2 , 1 ); 11 Действие - очистка корзины target . Clear() ; 11 Утверждение Assert .Equal(O , target.Lines . Count()) ; Временами , как в данном случае , код для тестирования функциональности класса получает­ ся намного длиннее и сложнее, чем код самого класса . Не допускайте, чтобы это приводило к отказу от написания модульных тестов. Дефекты в простых классах, особенно в тех, кото­ рые играют настолько важную роль, как Cart в приложении SportsStore, могут оказывать разрушительное воздействие. Создание кнопок добавления в корзину Нам необходимо модифицировать частичное представление Views / Shared/ ProductSummary. cshtml. добавив кнопки к спискам товаров. Чтобы подготовиться к этому, добавьте в папку Infrastructure файл класса по имени UrlExtensions . cs с определ ением расширяющего метода, приведенного в листинге 9.14. Листинг 9.14 . Содержимое файла UrlExtensions. cs из папки Infrastructure using Microsoft . AspNetCore.Http ; namespace SportsStore .I nfrastructure puЫic static class UrlExtensions { puЫic static string PathAndQuery(this HttpRequest request) => request.QueryString.HasValue ? ${request . Path}{request . QueryString} : request.Path.ToString(); Расширяющий метод PathAndQuery () оперирует над классом HttpRequest, ис­ пользуемый инфраструктурой ASP.NET для описания НТТР-запрос а. Расширяющий метод генерирует URL, по которому браузер будет возвращаться после обновления кор­ зины. принимая во внимание строку запроса. если она есть. В листинге 9.15 к файлу импортирования представлений добавляется пространство имен , которое содержит расширяющий метод. так что его можно применять в частичном представлении. Листинг 9.15. Добавление пространства имен в файле_Viewimports. cshtml @using SportsStore.Models @using SportsStore.Models . ViewMode ls @using SportsStore.Infrastructure @addTagHelper * , Microsoft . AspNetCore.Mvc . TagHelpers @addTagHelper SportsStore.Infrastructure.* , SportsStore
Глава 9. SportsStore: навигация 267 В листинге 9.16 показано частичное представление, описывающее каждый товар, которое обновлено для включения кнопки Add То Cart (Добавить в корзину). Листинг 9.16. Добавление кнопки в файле ProductSummary. cshtml @model Product <div class=well> <hЗ> <strong>@Model . Name</strong> <span class=pull-right label label - primary> @Model.Price.ToString(c) </span> </hЗ> <form id=@Model.ProductID asp-action=AddToCart asp-controller=Cart rnethod=post> <input type=hidden asp-for=ProductID /> <input type=hidden name=returnUrl value=@ViewContext.HttpContext.Request.PathAndQuery() /> <span class=lead> @Model.Description <button type=suЬmit class=btn btn-success btn-srn pull-right> Add То Cart </button> </span> </forrn> </div> Мы добавили элемент form, содержащий скрытые элементы input, которые ус­ танавливают значе ние ProductID из модели представления и URL, куда браузер должен возвращаться после обновления корзины. Элемент form и один из элементов input конфигурируются с использованием встроенных дескрипторных вспомогатель­ ных классов, что является удобным способом генерирования форм, которые содержат значения модели и нацелены на контроллеры и действия в приложении , как описано в главе 24. Во втором элементе input применяется расширяющий метод, созданный для установки URL возврата. Кроме того, добавлен элемент button, который будет отправлять форму прило жению. На заметку! Обратите внимание, что атрибут method элемента form установлен в post, что инструктирует браузер относительно отправки данных формы с использованием НТТР-метода POST . Вы можете это изменить и заставить форму применять НТТР -м етод GET, но дол ж ны соблюдать осторожность . Спецификация НТТР требует, чтобы запросы GET были идемпотентными, т.е . они не должны приводить к изменениям, а добавление товара в корзину определенно считается изменением. Более подробно эта тема будет обсуждаться в главе 16, включая объяснение того, что может произойти, если проигнори­ ровать требование идемпотентности запросов GET. Включение поддержки сеансов Мы собираемся сохранять детали корзины пользователя с использованием состоя­ ния сеанса, что представляет собой данные, которые хранятся на сервере и ассоции­ руются с последовательностью запросов, сделанных пользователем. Инфраструктура
268 Часть 1. Введение в инфраструктуру ASP.NET Core MVC ASP.NET предлагает целый ряд разных способов хранения состояния сеанса, в том числе хранение его в памяти, что мы и будем применять. Преимуществом такого под­ хода является простота , но данные сеанса будут утеряны, когда приложение останав­ ливается или перезапускается. Прежде всего, к приложению SportsStore понадобится добавить несколько новых па­ кетов NuGet. В листинг е 9.17 демонстрируются добавления в файле proj ect . j son. Листинг 9.17. Добавление пакетов в файле project. j son внутри проекта SportsStore dependencies : { Microsoft . NETCore . App : version: 1.0.0, type : platform }1 Mic r osoft . AspNetCore.Diagnostics : 1.0 .0, Microsoft.AspNetCore.Server.IISintegration: 1.0. О , Microsoft . AspNetCore.Server.Kes t r el: 1 . 0 . 0 , Microsoft . Extensions . Logging . Conso le: 1 . 0 . 0 , Microsoft.AspNetCore.Razor.Tools : { }1 version : l . 0 . 0-preview2-fi nal , type : build Microsoft . AspNetCore.Static Fi le s: 1.0.О , Microsoft . AspNetCore .M vc : 1. 0 . 0 , Microsoft . EntityFrameworkCore .SqlServe r : 1.0 .0, Microsoft . EntityFrameworkCore . Tools: l . 0 . 0-preview2-final , Microsoft.Extensions.Configuration.Json: 1.0 .0, Microsoft.AspNetCore.Session: 1.0 .0, Microsoft.Extensions.Caching.Mernory: 1.0.О, Microsoft.AspNetCore . Http.Extensions: 1.0.0 }1 Включение поддержки сеансов требует добавления в класс Startup служб и про­ межуточного программного обеспечения (листинг 9.18). Листинг 9.18. Включение поддержки сеансов в файле Startup. cs usi ng Microsoft . AspN etCore.Bui lder ; usi ng Microsoft . AspNetCore . Hosting; using Microsoft . Asp NetCore . Http ; us ing Microsoft . Extensions . Dependencyi nject ion ; us ing Microsoft . Extensions . Logging ; using SportsStore . Models ; us ing Microsoft . Extensions . Configu rati on ; usi ng Microsoft . EntityFrameworkCo re; namespace SportsStore { puЫic class Startup { IConfigurationRoot Configura ti o n; puЫic St artup(IHostingEnvironment env) { Configuration = new ConfigurationBuilder()
Глава 9. SportsStore: навигация 269 . SetBasePath(env . ContentRootPath) . Ad dJsonFile(appsettings .j son) . Build() ; puЫic void ConfigureServices(IServiceCollection services) services . AddDbCon t ext<ApplicationDbContext>(options => options . UseS q lServer( Configurat i on[Data : SportStore Pr od u cts : Connection String ] )) ; services . AddTransient<IProductRe pos i tory , EFProductRepos itory>() ; services . AddMvc() ; services.AddМemoryCache(); services.AddSession(); puЫic void Configure( I ApplicationBui lder арр , IHostingEnvironment e n v , I LoggerFacto r y loggerFacto r y) { app . UseDeveloperExcept i o n Page() ; app . UseSta t us CodePages() ; app . UseStati c Files() ; app.UseSession(); app . UseMvc(routes => { 11 .. . для краткости конфигурация маршрутизации не показана . . . }); SeedData. En surePopu l ated(app) ; Вызов м етод а AddMemoryCache () настраивает хранилище данных в памяти. М ето д AddSession () регистр и руе т служ бы, используемые для доступа к данным се ­ анса , а метод UseS e ssion () позволяет системе сеансов автоматичесии ассоцииро­ вать з апросы с сеансами, когда они поступают от клиента . Реализация контроллера для корзины Для обработки щелчков на кнопках Add То Сагt понадобится создать контроллер . Доб авьте в папку Controllers файл класса по имени CartContro ll er . cs с опр еде­ ле нием , пр едставленным в листинг е 9.19. Листинг 9.19. Содержимое файла CartController. cs из папки Controllers using System .Linq ; using Microsof t. AspNetCore . Ht t p ; using Microsoft . AspNetCore . Mvc ; using SportsStor e . Infrastructure ; using SportsStore . Models ; namespace SportsS t ore . Cont r ol l ers puЬlic class CartContr oll e r : Controlle r pri vate IProductRe pos i tory repos itory ; puЫic Car t Contro ll er( IP roductRepo sitory repo) { repository = repo ;
270 Часть 1. Введение в инфраструктуру ASP.N ET Саге MVC puЫic RedirectToActionResult AddToCart(int productid, string returnUrl) { Product product = repository.Products . FirstOrDefault(p => p . ProductID == productid) ; i f (product != null) { Cart cart = GetCart(); cart.Additem(product , 1) ; Savecart(cart) ; return RedirectToAction(Index , new { returnUrl )) ; puЬlic RedirectToActionResult RemoveFromCart(int productid , string returnUrl) { Product product = r epository . Products . FirstOrDefault(p => p . Prod uctI D == productid); if (product != null) { Cart cart = GetCart() ; cart.RemoveLine(product); SaveCart(cart); return RedirectToAction(Index , new { returnUrl )) ; private Cart GetCart() { Cart cart = HttpContext.Session . GetJson<Cart >(Cart) ?? new Cart() ; retu rn cart ; private void SaveCart(Cart cart) { HttpContext.Session.SetJson(Cart, cart); Относительно этого контроллера необходимо сделать несколько замечаний. Для сохранения и извлечения объ е ктов Cart применяется средство состояния сеанса ASP.NET, с которым и взаимодействует метод GetCart (). Промежуточное програм­ мное обеспечение , зарегистрированное в предыдущем раздел е, использует сооkiе­ наборы или переписывание URL, чтобы ассоциировать вместе множество запросов от определенного пользователя с целью формирования отдельного сеанса просмотра . Связанным средством является состояние сеанса, которое ассоциирует данные с сеан­ сом. Это идеально подходит для класса Cart: мы хотим, чтобы каждый поль зователь имел собственную корзину, и эта корзина сохранялась между запросами. Данные. связанные с сеансом, удаляются по истечении времени существования сеанса (обыч­ но из-за того , что пользователь не отправляет запрос какое-то время) , т.е . управлять хранилищем или жизненным циклом объектов Cart не придется. В методах действий AddToCart () и RemoveFromCart () применялись имена пара­ метров, которые соответствуют именам элементов input в НТМL-формах , созданных в представлении ProductSummary. cshtml . Это позволяет инфраструктуре МVС ассо­ циировать входящие переменные НТТР-запроса POST формы с параметрами и означа ­ ет, что делать что-то самостоятельно для обработки формы не нужно. Такой процесс называется привязкой модели и с его помощью можно значительно упрощать классы контроллеров, как будет объясняться в главе 26.
Глава 9. SportsStore: навигация 271 Определение расширяющих методов состояния сеанса Средство состояния сеанса в ASP.NET Core хранит только значения int, string и byte []. Поскольку мы хотим сохранять объект Cart, необходимо определить рас­ ширяющие методы для интерфейса ISession, которые предоставят доступ к дан­ ным состояния сеанса с целью сериализации объектов Cart в формат JSON и их об­ ратного преобразования. Добавьте в папку Infrastructure файл класса по имени SessionExtensions . cs с определениями расширяющих методов, показанными в листинге 9.20. Листинг 9.20. Содержимое файла SessionExtensions. cs из папки Infrastructure using Microsoft . AspNetCore.Http; using Microsoft .Asp NetCore . Http . Features; using Newtonsoft . Json; namespace SportsStore . Infrastructure { puЬlic static class SessionExtensions puЫic static void SetJson(this ISession session, string key , object value) session . SetString(key , JsonConvert . SerializeObject(value) ) ; puЬlic static Т GetJson<T>(this ISession session , string key) { var sessionData = session.GetString(key); return sessionData == null ? default(T) : JsonConvert.DeserializeObject<T>(sessionData); При сериализации объектов в формат JSON (JavaScript Object Notation - систе­ ма обозначений для объектов JavaScript) эти методы полагаются на пакет Json.NET (глава 20). Пакет Json.NET не потребуется добавлять в файл package. j son, т.к. он уже используется "за кулисами" инфраструктурой MVC для поддержки средства заго­ ловков JSON, которое описано в главе 21. (И нформация о работе с пакетом Json.NET напрямую доступна по адресу www . newtonsoft . сот/ j son.) Расширяющие методы делают сохранение и извлечение объектов Cart очень лег­ ким . Для добавления объекта Cart к состоянию сеанса в контроллере применяе тся следующий вызов: HttpContext . Session .SetJson (Cart , cart) ; Свойство HttpContext определено в базовом классе Controller, от которого обычно унаследованы контроллеры, и возвращает объект HttpContext . Этот объект предоставляет данные контекста о запросе, который был получен, и ответе, находя­ щемся в процессе подготовки. Свойство HttpContext. Session возвращает объект, реализующий интерфейс ISession. Данный интерфейс является именно тем типом, где мы определили метод SetJson (), принимающий аргументы, в которых указы-
272 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC ваются ключ и объект, подлежащий добавлению в состояние сеанса . Расширяющий метод сериализирует объект и добавляет его в состояние сеанса, используя функцио­ нальность, которая лежит в основе интерфейса ISession. Чтобы извлечь объект Cart, применяется другой расширяющий метод, которому указывается тот же самый ключ: Cart cart = HttpContext.Session.GetJson<Cart>(Cart); Параметр типа позволяет задать тип объекта, который ожидается извлечь; этот тип используется в процессе десериализации. Отображение содержимого корзины Финальное замечание о контроллере Cart касается того, что методы AddToCart () и RemoveFromCart () вызывают метод RedirectToAction (). Результатом будет от­ правка клиентскому браузеру НТГР-инструкции перенаправления, которая заставит браузер запросить новый URL. В данном случае браузер запросит URL, который вы­ зывает метод действия Index () контроллера Cart. Мы планируем реализовать метод Index () и применять его для отображения со­ держимого объекта Cart. Если вы еще раз взглянете на рис. 9.7, то увидите, что это та часть рабочего потока, которая инициируется щелчком пользователя на кнопке добавления в корзину. Представлению, которое будет отображать содержимое корзины, необходимо пе­ редать две порции информации: объект Cart и URL для отображения в случае, если пользователь щелкнет на кнопке Continue shopping (Продолжить покупку). Для этой цели мы создадим простой класс модели представления. Добавьте в папку Models/ ViewModels проекта SportsStore файл класса по имени CartindexViewModel. cs с содержимым, приведенным в листинге 9.21 . Листинг 9.21. Содержимое файла CartindexViewModel. cs из папки Models/ViewModels using SportsStore.Models; namespace SportsStore.Models.ViewModels puЫic class CartindexViewModel puЫic Cart Cart { get ; set; } puЬlic string ReturnUrl { get ; set; Имея модель представления, можно реализовать метод действия Index () в нлассе CartController, нан поназано в листинге 9.22 . Листинг 9.22 . Реализация метода действия Index () в файле CartController. cs using System.Linq; using Microsoft .AspNe tCore.Http ; using Microsoft .A spNetCore.Mvc ; using SportsStore.Infrastructure; using SportsStore.Models; using SportsStore.Models.ViewModels;
namespace SportsStore . Co nt ro l lers { puЫic class CartControl l er : Controller private I P roductRepositor y reposito r y ; Глава 9. SportsStore: навигация 273 puЫic CartControl l er( I P r od u ctRepo s itory repo) { repository = repo; puЫic ViewResult Index (string returnUrl) return View(new CartindexViewModel { Cart = GetCart(), ReturnUrl = returnUrl }); // .. . для краткости другие методы не показаны . .. Действи е Index извлекает объ ект Cart из состояния с е анса и использует его для со здания объе1па Car t indexViewModel, которы й зате м пер едается методу View () для применения в качестве модели представления. Посл едний шаг при отображении содержимого кор зины предусматрива ет созда ­ ние пр едставл ения , которое будет визуализировать действие I ndex. Со здайте папку Vi ews/Cart и поместит е в н ее файл представления Razor по им е ни Index . csh t rnl с ра змет кой, приведенной в листинг е 9.23. Листинг 9.23. Содержимое файла Index. cshtml из папки Views/Cart @model CartindexViewMo d el <h2>Your cart</h2> <tаЫе c l ass= t a Ы e taЬle - bordered taЬle - striped> <thead> <tr> < th>Qua n tity</th> <th>Item</th> <th class=text- r i g h t>Price</ t h> <th class=text- r i g ht>S ubtotal</t h> </tr> </thead> <tbody> @foreach (var line in Model .Car t .Lines) { <tr> <td class = t e xt - center>@line . Quant i ty</td> <td class=te xt - left>@line . Prod u ct . Name</td> <td class=text- r i ght>@l i ne . Pr oduct.Price.ToString(c)</td> <td cl ass=t ext- right> @((line.Quantity * line.Product.Price).ToString(c)) </td> </tr> ) </tbody>
274 Часть 1. Введение в инфраструктуру ASP.NET Core MVC <tfoo t > <tr > <td colspan=З cl ass=text-right>Total:</t d> <t d class=t ex t- right> @Model .Cart .ComputeTot al Val ue () .ToSt ring(c) </td> </tr> </tfoot> </tаЫе> <div class=text- center > <а cl ass=bt n btn-primary href=@Model .ReturnUrl>Cont inue shopping</a> </div> Представление проходит по элементам в корзине и добавляет в НТМL-таблицу строку для каждого элемента вместе со стоимостью и итоговой суммой по корзине . Классы, назначенные элементам, соответствуют стилям Bootstrap для таблиц и вы­ равнивания текста . В результате доступна базовая функциональность корзины для покупок. Во­ первых, товары выводятся вместе с кнопками Add То Cart (Добавить в корзину), как показано на рис. 9.8 . Во-вторых, щелчок пользователя на кнопке Add То Cart приводит к добавлению со ­ ответствующего товара в его корзину и отображению сводной информации по корзи­ не (рис. 9.9). Щелчок на кнопке Continue shopping (Продолжить покупку) возвратит на страницу товара , из которой произошел переход . D SportsStor. х С [2 l~calh~t:бO_ooo..:;_=;c=~.=,=,fP=age 1 =-==-------==----= fJ SPO! !TS STOHE Нотв Thinking Сар Socce r lmprove brai 11efficiency Ьу 75% Watersport s Unsteady Cha ir lf.VИJ Secretly glve your opponent а disadvantage Рис. 9.8 . Кнопки Add То Cart
1 1 1 Глава 9. SportsStore: навигация 275 С] SportsStor~ Х ~ с @)-io~~~·~0ooo/c;~"1nd~~'::"~~~~2~~~~--~~=~=---~·==-~=--=~ Sf'ofПS STOllE. Home Your cart Chess lt em Pri ce SuЫ ota l 1 Socce r Quantity 51 6.00 $16.00 1 . 1 ! Tl1 inking Сар WaterspoГis Uns tea dy Chai r 529 .95 S29 .95 ! Huma n Chess Вoard 575.00 S75.00 1 Knyak $275.00 $275.00 • : 1 1 Tolal: $395.95 . l--=--J Рис. 9.9 . Отображение содержимого корзины для покупок Резюме В настоящей главе мы начали расширять пользовательские части приложения SportsStore. Мы предоставили средства. с по мощью которых пользователь может пе­ реходить по категориям, и создали базовые строительные блоки, позволяющие добав­ лять элементы в корзину для покупок. В следующей главе разработка приложения будет продолжена.
ГЛАВА 10 SportsStore: завершение постр оения корзины для покуп ок в настоящей главе мы продолжим строить пример приложения SportsStore. В пре­ дыдущей главе мы добавили базовую поддержку I{Qрзины для покупок, а теперь собираемся усовершенствовать и завершить создание этой функциональности. Усовершенствование модели корзины с помощью службы В предыдущей главе был определен класс модели Car t и продемонстрирована воз­ можность его сохранения с использованием средства состояния сеанса, что давало возможность пользователю формировать набор товаров для покупки . Обязанность по управлению постоянством класса Cart возлагалась на контроллер Cart , в котором явно определялись методы для получения и сохранения объектов Cart. Проблема такого подхода в том , что нам пришлось дублировать код для получения и сохранения объектов Car t во всех компонентах, которые его применяли . В этом разделе мы собираемся использовать средство служб, находящееся в самой основе инфраструктуры ASP.NET Core, чтобы упростить способ, которым управляются объ­ екты Ca rt, освобождая индивидуальные компоненты, такие как контроллер Cart , от необходимости напрямую иметь дело с деталями. Службы чаще всего применяются для сокрытия деталей реализации интерф ейсов от компонентов , .которые от них зависят. Вы видели пример, когд а создавалась служ ­ ба для интерфейса IP r o duct Repo si t o ry, которая позволила гладко заменить фик ­ тивный класс хранилища реальным хранилищем Entity Framework Core . Но службы могут использоваться для решения множества других задач, а также применяться для придания и изменения формы приложения, даже когда работа производится с конк­ ретными классами наподобие Cart. Создание класса корзины, осведомленного о хранилище Первым шагом по приведению в порядок способа использования класса Cart будет создание подкласса, который осведомлен о том, как сохранять самого себя с применением состояния сеанса . Добавьте в папку Mode l s файл класса по имени SessionCa r t . cs и поместите в него определение, показанное в листинге 10.1 .
Глава 1О. SportsStore : завершение построения корзины для по ку пок 277 Листинг 10.1 . Содержимое файла SessionCart. cs из папки Models using System ; using Microsoft . AspNetCore .H ttp ; using Microsoft . Extensions . Dependency inj ection ; using Newtonsoft . Json; using SportsStore . Infrastructure ; namespace SportsStore . Models { puЫic class SessionCart : Ca r t puЫic static Cart GetCart(IService Provider services) { ISession session = services . GetRequiredService<IHttpContextAccessor>()? . HttpContext.Session ; SessionCart cart = sess i on? . GetJson<SessionCart> ( " Cart " ) ?? new SessionCart() ; cart . Session = session ; return cart ; [Jsonignore] puЫic ISession Session { get ; set; } puЫic override void Additem(Product product , int quantity) { base.Additem(product , quantity) ; Session . SetJson( " Cart ", this) ; puЬlic override void RemoveLine(Product product) { base . RemoveLine(product) ; Session . SetJson( " Cart ", this) ; puЬlic override void Clear() base . Clear() ; Session . Remove( " Cart " ) ; } } Класс SessionCart является производным от класса Cart и переопределяет мето­ ды Addrtem () , RemoveLine () и Clear () ,так что они вызывают базовые реализации и затем сохраняют обновленное состояние в сеансе, используя расширяющие мето­ ды интерфейса ISession, которые были определ е ны в главе 9. Статический метод GetCart () - это фабрика для создания объектов SessionCart и их пр едоставления с помощью объекта реализации ISession, так что они могут себя сохранять. Получение объ екта реализации ISession несколько затруднено. Мы должны по­ лучить экземпляр службы IHttpContextAccessor , который предоставит доступ к объекту HttpCont ext , а тот, в свою очередь, к объекту реализации ISession. Такой окольный подход т ребу е тся из -за того, что сеанс не пр едоставляется как обычная служба . Ре гистрация службы Сл едующий шаг заключается в создании службы для rшасса Cart . Цель в том, чтобы удовлетворять запросы для объектов Cart выдачей объектов SessionCart, которые будут сохранять себя самостоятельно. Создание службы иллюстрируется в листинге 10 .2 .
278 Часть 1. Введение в инфраструктуру ASP.NET Со ге MVC Листинг 10.2. Создание службы корзины в файле Startup. cs puЬlic void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDb Context>(options => opt i ons . UseSqlServer( Configuration["Data : SportStoreProducts:ConnectionString " J)) ; services . AddTransient<IProductRepository , E FProductRepository>() ; services.AddScoped<Cart>(sp => SessionCart.GetCart(sp)); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services . AddMvc(); services . AddMemoryCache() ; services . AddSession() ; Метод AddScoped () уиазывает, что для удовлетворения связанных запросов к эк­ земплярам Cart должен применяться один и тот же объект. Способ связывания за­ просов может быть сконфигурирован, но по умолчанию это значит, что в ответ на любой запрос энземпляра Cart со стороны компонентов , ноторые обрабатывают тот же самый НТГР-запрос, будет выдаваться один и тот же объект. Вместо предоставления методу AddScope d () отображения между типами, как делалось для хранилища, уназывается лямбда-выражение, которое будет вьmолнять­ ся для удовлетворения запросов к Cart . Лямбда-выражение получает коллекцию служб, которые были зарегистрированы, и передает ее методу GetCart () класса SessionCart . В результате запросы для службы Ca r t будут обрабатываться путем создания объектов Sess i onCart , которые сериали зируют сами себя как данные се­ анса, когда они модифицируются . Мы также добавили службу с использованием метода AddSingleton () ,которы й указывает, что всегда должен применяться один и тот же объект. Со зданная служба сообщает инфраструктуре MVC о том, что когда требуются реализации интерфейса IHttpConte xtAccessor , необходимо использовать класс HttpContextAccessor. Данная служба обязательна, поэтому в классе SessionCart можно получать доступ к текущему сеансу , как делалось в листинге 1О . 1. Упрощение контроллера Cart Преимущество создания службы такого вида свя за но с тем, что она позволит уп ­ ростить контроллеры , в которых применяются объекты Cart. В листинге 10.3 приве ­ ден переделанный класс CartControl l er , где задействована нов а я служба. Листинг 10 .3. Использование службы Cart в файле CartController. cs using System .L inq ; using Microsoft.AspNetCore . Mvc ; using SportsStore.Models ; using SportsStore.Models . ViewMode ls; namespace SportsStore . Controllers { puЫic class CartController : Co ntrol ler private IProductRepository repository ; private Cart cart ;
Гл ава 1О. SportsStore: завер ш ение построения к орзины д ля по купок 279 puЫic CartControl ler (I ProductRepo sitory repo , Cart cartService) repository = repo ; cart = cartService ; puЫic ViewResu l t Index(string returnUrl) return View( ne w CartindexViewMode l { Cart = cart , ReturnUrl = returnUrl }); puЬlic RedirectToActionResult AddToCart( int product i d , string returnUr l) { Product product = repository . Products . FirstOrDefault(p => p . ProductID == produ ctid) ; if (product ! = null) { cart.Additem(product , 1) ; return RedirectToAction( "Index", new { r eturnUrl }) ; puЬlic RedirectToActionResult RemoveFromCart(int productid , string returnUr l) { Prod uct product = repository . Products .FirstOrDefault(p => p.ProductID == productid) ; if {product != null) { cart . RemoveLine(product) ; return RedirectToAction( "Index " , new { returnUrl }) ; Класс CartController указывает на то, что он нуждается в объекте Cart, за счет объявления аргумента конструктора. Это позволяет удалить методы, которые читают и записывают данные в сеанс, а также код, требующийся для записи обновлений. Результатом явля ется контроллер , который не только проще , но и более сосредоточен на своей роли в приложении, не беспокоясь о том , как объекты Cart создаются или хранятся. И поскольку службы доступны по всему приложению, любой компонент мо­ жет получать корзину пользователя с применением одного и того же приема. Завершение функциональности корзины После ввода службы Cart наступило врем.я завершить построение функциональ­ ности корзины, добавив два новых средства . Первое средство позволит поль зователю удалять элемент из корзины. Второе средство даст возможность отображать итоговую информацию по корзине в верхней части страницы. Удаление элементов из корзины Мы уже определили и протестировали метод действия Remove FromCart () в кон ­ троллере, поэтому для предоставления пользователям возможности удаления эле­ ментов достаточно лишь открыть доступ к данному методу в представлении, доба­ вив 1шопки Remov e (Удалить) ко вс ем строкам в итоговой информации по корзине . Изменения , которые понадобится внести в файл Views/Cart/ I ndex . cshtml, пока­ заны в листинге 10.4.
280 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 10.4 . Добавление кнопок Remove в файле Index.cshtml из папки Views/Cart @model Ca r tindexViewModel <h2>Your cart</h2> <tаЫе class= " taЫe taЬle - bordered taЬle - striped " > <thead> <tr> <th>Quantity</th> <th> Item</th> <th class= " text - right " >Price</th> <th class= " text - right " >SuЬtotal</th> </tr> </thead> <tbody> @foreach (var line in Model . Cart .Lines) { <tr> <td class= " text - center " >@line . Qu antity</td> < td class= " text -l eft " >@ line.Product.Name</td> <td class= " text-right " >@ l ine . Product.Price .T oString( " c " )</td> <td class= " text - right " > @( (line . Quantity * line. Product . Price) . ToString ( " с " )) </td> <td> <form asp-action="RemoveFromCart" method="post"> <input type="hidden" name="ProductID" value="@line.Product.ProductID" /> <input type="hidden" name="returnUrl" value="@Model.ReturnUrl" /> <Ьutton type="suЬmit" class="Ьtn Ьtn-sm Ьtn-danger "> Remove </button> </form> </td> </tr> </tbody> <tfoot> <tr> <td colspan= " З " c l ass= " text-right " >Total : </ td> <td class= " text - right " > @Mode l .Cart.ComputeTotalValue() . ToString( " c " ) </td> </tr> </tfoot> </tаЫе> <div class= " text - center " > <а class= " Ьtn Ьtn-prirnary " href= " @Model . ReturnUrl " >Continue shopping</a> </div> Мы добавили J\ каждой строке таблицы новый столбец, содержащий элемент form со скрытыми элементами input , которые указывают товар, подл ежащий удалению, и URL возврата , а также кнопку для отправки формы .
Глава 1О. SportsStore: завершение nостроения корзины для nокуnок 281 Чтобы увидеть кнопки Remove в работе. запустите приложение и добавьте несколь­ ко элементов в корзину для покупок. Поскольку корзина уже обладает функциональ­ ностью удаления элементов, ее можно протестировать, щелкнув на одной из новых кнопок (рис. 10.1). IJ Sport$Stor~ ~ с [~~ r;,~1~~~~~~-~;~~:~?~;~;~.n::~~~s~;-~~~~~~~~-~-=·.~~==--· _~l Hom o Chess Soccer Wat e,sports Your cart Ou onlity ltem Price SuЬtota l Thlnking Сар S 16.00 $ 16.00 Unsleady Cl1В ir S29 .95 S29.95 Stadium $79,500.00 $79,500 .00 Total : $79,545 .95 Fфjфii.ii , .!фi " "1111 $79.500 .00 $79.500.00 1 1 Total: $79.5 16 .00 lав"111 '" "''....."''-"'''~'1<1~~~i!li!!!~lifll!ll~"~'-"'~~~~~~~~ ~- ~ .i " 11111 l·.~~~~~~~~~~~~:1.~--~1 Рис. 10.1. Удаление элемента из корзины для покупок Добавление виджета с итоговой информацией по корзине Мы имеем функционирующую корзину , но еще должны определить способ ее встра­ ивания в пользовательский интерфейс . Пользователи могут выяснять, что находится в их корзинах, только за счет просмотра экрана со сводкой по корзине. А попасть на этот экран можно только путем добавления нового элемента в корзину. Для решения упомянутой задачи мы создадим виджет (графический элемент) с итоговой информацией по содержимому корзины, щелчок на котором будет приво­ дить к отображению товаров, находящихся в корзине, и сделаем его доступным в любом месте приложения . Он будет реализован почти так же, как виджет для нави­ гации - в виде компонента представления, вывод которого может быть включен в разделяемую компоновку Razor. Добавление пакета Font Awesome В качестве части итоговой информации по корзине мы будем отображать кнопку. которая позволит пользователю перейти к оплате. Вместо отображения каких-либо слов на кнопке мы хотим использовать символ корзины. При отсутствии художественных навыков можно применить пакет с открытым кодом Font Awesome, предлагающий вели­ колепный набор значков, которые допускается интегрировать в приложения как шриф­ ты. где каждый символ шрифта представляет собой отдельное изображение. Получить дополнительные сведения о пакете Font Awesome, а также просмотреть содержащиеся в нем значки, можно по адресу http: / /fortawesome. github. i o/Font-Awesome .
282 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Выберите проект SportsStore и щелкните на кнопке Show All ltems (Показать все элементы) в верхней части окна Solution Explorer, чтобы отобразить ф айл bower . j s on. Добавьте пакет Font Awesome в раздел dependencies этого файла (листинг 10.5). Листинг 10.5 . Добавление пакета Font Awesome в файле bower. j son "narne": "asp.net ", "private": t rue, " dependencies ": { "bootstrap": "3 .3 .6", "fontawesome": "4.6.3" После сохранения файла bower . j son среда Visual Studio с помощью инструмента Bower загрузит и установит пакет Font Awesome в папку www/liЬ/fontawesorne. Создание класса компонента представления и представления Добавьте в папку Comp onents файл класса по имени CartSummaryViewComponent . cs и определите в нем компонент представления, показанный в листинг е 10.6 . Листинг 10.6 . Содержимое файла CartSummaryViewComponent. cs из папки Components using Microsof t .AspNetCore .Mvc ; using SportsStore . Models ; n arnespace Spo r tsStore . Corn p onents puЬl i c c l ass Cart SurnrnaryViewCornponent : ViewComponen t private Ca r t cart ; puЬlic Cart SurnrnaryViewCornponent (Car t car tSer vi ce) { cart = cartService ; puЬl i c IVi e wCo rn ponentResult Invoke() { return Vi ew(ca r t) ; Этот компонент представления способен задействовать в своих интересах службу , созданную ранее в главе для получения объекта Cart , принимая ее как аргум ент конс ­ труктора. Результатом оказывается простой компонент представления, который пере ­ дает объект Cart методу View () ,чтобы сг енерировать фрагмент НТМL-разм етки для включения в компоновку. Чтобы создать компоновку, создайте папку Views/Shar ed/ Components/CartSummary, добавьте в не е файл представления Razor по имени Defaul t . c s html и поместите в этот файл разметку, приведенную в листинге 10.7.
Глава 1О. SportsStore: завершение построения корзины для покупок 283 Листинг 1О.7. Содержимое файла Defaul t. cshtml из папки Views/Shared/Components/CartSummary @model Cart <div class= "" > @if (Model . Lines . Coun t(} > 0) <small class= " navbar -tex t " > <b>Your cart:</b> @Model . Lines.Sum(x => x.Quanti ty) item(s) @Model . ComputeTotalValue() . ToString( " c " ) </small> <а class= "Ьtn Ьtn - sm Ьtn - defau lt navbar-Ьtn " asp - controller="Cart" asp-action= " Index" asp- route-returnurl="@ViewContext .HttpContext . Request . PathAndQuery() " > <i class= " fa fa - shopping-cart " ></ i> </а> </div> Представление отображает кнопку со значком корзины Font Awesome и, когда в кор­ зине присутствуют товары, пр едоставляет снимок, который сообщает количество эле­ ментов и общую сумму . Теперь, имея компонент представления и представление, мож­ но модифицировать разделяемую компоновку. чтобы итоговая информация по корзине включалась в ответы, генерируемые контроллерами приложения (листинг 10.8) . Листинг 10.8. Добавление итоговой информации по корзине в файле_Layout. cshtml < !D OCTYPE html> <html> <head> <meta name =" viewport " content="width=device - width" /> <link rel= "stylesheet " asp-href-include= " liЬ/bootstrap/dist/css/* . min.css" /> <link rel="stylesheet" asp-href-include="/lib/fontawesome/css/*.css" /> <title>SportsStore</title> </head> <body> <div class= " navbar navbar-inverse " r ole= " navigation"> <а class= " navbar - b rand" href= " # " >SPORTS STORE</a> <div class="pull-right"> @awai t Component. InvokeAsync ( "CartSuпnnary") </div> </div> <div class= " row panel " > <div id=" categories " class= " col-xs-3 " > @await Component.InvokeAsync("NavigationMenu " ) </div> <di v class="col- xs- 8 "> @RenderBody () </div> </div> </body> </html>
284 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Запустив приложение, можно увидеть итоговую информацию по корзине. Когда корзина пуста, отображается только кнопка перехода к оплате. По м е ре добавле­ ния элементов в корзину выводится их количество и общая стоимость (рис. 10.2). Благодаря такому дополнению пользователь знает, какие товары находятся в корзине, и получает очевидный путь для перехода к оплат е покупок. yak ,oat for one person ~" .,,...__ .-Ь,,..,,,.......,,... .,....U " •" • ф ••trnь", Рис. 10.2. Отображение итоговой информации no корзине Отправка заказов Итак, мы добрались до финального пользовательского средства приложе ния SportsStore: возможности пер ехода к оплате и оформлению заказа. В последующих разделах мы расширим модель предметной области , чтобы обеспечить поддержку по­ лучения от пользователя подробной информации о достав1<е . и добавим в приложение обработку этой информации. Создание класса модели Добавьте в папку Mode l s файл класса по имени Orde r. c s и приведите его содер­ жимое в соответствие с листингом 10.9. Этот класс будет использоваться для пр ед­ ставления информации о доставке пользователю. Листинг 10.9. Содержимое файла Order. cs из папки Models using System . Co l lect i ons . Generic ; us ing System . ComponentModel . DataAnnot ations ; u sing Microsof t. AspNetCore . Mvc . Mod elBinding ; namespace SportsSto r e . Mode l s puЫic class Order { [BindNever] puЫic int OrderID { ge t; se t; } [BindNeve r ] puЬlic I Coll ection<Car t Line> Lines { get ; set; } [Required(ErrorMessage = " Pl ease enter а name" )] 11 Введите имя
Глава 1О. SportsStore : завершение построения корзины для покупок 285 puЫic string Name { get ; set; ) [Required(ErrorMessage = "Pl ease enter the first add r ess line")] puЫic string Linel puЫic string Line2 puЫic string LineЗ //Введите первую строку адреса get; set; ) get; set; } get; set; ) [Required(ErrorMessage = "Please enter а city name " )] 11 Вв едите название города puЫic string Ci ty { get; set; ) [Required(ErrorMessage = " Please enter а state name " )] 11 Введите название штата puЫic string State { get; set ; ) puЫic string Zip { get ; set; ) [Required (ErrorMessage = "P lease enter а country name")] 11 Введите название страны puЫic string Country { get ; set; ) puЫic bool GiftWrap { get ; set ; ) Здесь применяются атрибуты проверки достоверности из пространства имен Systern . CornponentModel . DataAnnotations, как мы делали в главе 2. Провер1<а до­ стоверности подробно рассматривается в главе 27. Кроме того, используется атрибут BindNever , который предотвращает предо­ ставление пользователем значений для снабженных им свойств в НТТР-запросе. Это ср едство системы привязки моделей, которая описана в главе 26. Добавление реализации процесса оплаты Наша цель заключается в том, чтобы обеспечить пользователям возможность ввода информации о доставке и отправки заказа. Для начала потребуется добавить к представлению итоговой информации по корзине кнопку Checkout (Перейти к оп­ лате). Изменения, которые необходимо внести в файл Views/Cart/Index . cshtrnl, показаны в листинг е 10.10. Листинг 10.10. Добавление кнопки Checkout в файле Index. cshtml из папки Views/Cart <div class= " text - center " > <а class= "btn Ьtn -p rima r y " href="@Model . ReturnUrl " >Continue shopping</a> <а class="Ьtn Ьtn-primary" asp-action="Checkout" asp- controller="Order"> Checkout </а> </div> Изменения обеспечивают генерацию ссылки, стилизованной в виде кнопки, щел­ чок на которой приводит к вызову метода действия Checkout () контроллера Order, создаваемого в следующем разделе. На рис. 10.З по1<азано, как выглядит эта кнопка .
286 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Ноте Your cart Chess Quantity ltem Price SuЫotaJ Socce r Ufejacket Sд8.95 $48.95 " Wat erspor ts Soccer Вall $19.50 $19.50 111 Рис. 10.З. Кнопка Checkout Теперь понадобится определить контроллер Order. Добавьте в папку Controllers файл класса по имени OrderController . cs с определением, приведенным в листин­ ге 10.11 . Листинг 10. 11 . С одержимое файла Or d erContro ller. c s из папки Controllers using Microsoft.AspNetCore . Mvc; using SportsStore . Models ; namespace SportsStore.Controllers puЫic class OrderController : Controller puЬlic ViewResult Checkout() => View(new Order()) ; Метод Checkout () возвращает стандартное представление и передает новый объ ­ ект ShippingDetails в качестве модели представления. Чтобы создать представле­ ние, создайте папку Views/Order и поместите в нее файл представления Razor по имени Checkout. cshtml с разметкой. п оказанной в листинге 10.12. Листинг 10. 12. С одержимое файла Check out. c shtml из папки Views /Or d er @model Order <h2>Check out now</h2> <p>Please enter your details, and we ' ll ship your goods right away!</p> <form asp - action= " Checkout" method="post" >
Глава 1О . SportsStore: завершение построения корзины для по к упо к 287 <hЗ>Ship to</hЗ> <div class= " form - group " > <label>Name:</labe l ><input asp-for="Name" class="form-control " /> </di v> <hЗ>Address</hЗ> <di v class= " form -group"> <label>Line 1 :</label><input asp -for="Linel" class="form-control " /> </div> <div class= "form-group " > <label>Line 2 :</labe l><inp ut asp-for="Line2 " class="form-control " /> </div> <div class= "form-group" > <label>Line 3 : </label><input asp -f or= " LineЗ" c lass = "form- control" /> </div> <div class= " form -gr oup "> <label>City : </label><input asp-for="City " class= " form - control" /> </div> <div class= "form - group " > <label>State:</label>< inp ut asp-for= "S tate " class= " form - control" /> </div> <div class= " form -group" > <label>Z ip:</label><input asp - for= " Zip " class= " form - cont rol" /> </div> <div class= " form -group " > <label>Country : </label><input asp-for= " Country" class="form-control" /> </div> <hЗ>Options</hЗ> <div class="checkbox"> <label> <input asp- f or="Gi ftWrap " /> Gift wrap these items </label> </div> <div class= " text - center " > <input class= " Ьtn Ьtn-primary " type= "submit" value= " Complete Order " /> </div> </form> Для каждого свойства в модели мы создали элементы label и input для поль­ зовательского ввода, сформатированные с помощью Bootstrap. Атрибут asp -for в элементах input обрабатывается встроенным дескрипторным вспомогательным классом, который генерирует атрибуты type , id, name и value на основе указанного свойства модели, как объясняется в главе 24. Чтобы увидеть результат добавления нового метода действия и представления (рис. 10.4). запустите приложение, щелкните на кнопке со значком корзины в верх­ ней части страницы и затем щелкните на кнопке Checkout (Оплата) . Попасть на это представление можно также, запросив URL вида /Order/Checkout. Реализация обработки заказов Мы будем обрабатывать заказы путем их записывания в базу данных. Разумеется, большинство сайтов электронной коммерции на этом не останавливаются, но мы не будем заниматься обработкой кредитных карт или других форм оплаты. Чтобы сосре ­ доточиться на MVC, вполне достаточно простого сохранения в базе данных .
288 Часть 1. Введение в инфраструктуру ASP. NEТ Core MVC С) SportsStore Х 1f- ·· С ГФ' 1ocalh;~;;QOO{o~-.~/Checka<1 1 ________________ __ " __ _ __ _*/ 1 ·-------------··----- ·-·---·- ·- - -- · -- - ··--· -"· --J Sl'OfHS ~~ТОНЕ а Horno Check out now Chess Pl ease en ter your details, and \'1e'll ship your goods rk;h t 8\vay! Socce r Waterspo r1s Ship to Name : Addr ess Une 1: Un e2: Un eз: City: State: Zip: Country; Options О Giftwrnp11-.eseitems .•",. Рис. 10.4. Форма для сбора деталей о доставке Расширение базы данных Когда на месте основной связующий код , созданный в главе 8 , добавить в базу дан ­ ных новый вид модели легко. Добавьте в класс контекста базы данных новое свойс­ тво , как показано в листинге 10.13. Листинг 10.13. Добавление свойства в файле ApplicationDЬContext. cs using Mi cr osof t .EntityFrarnewor kCore ; narnespace SportsStore .Models {
Глава 1О . SportsStore : завершение построения к орзины для покупок 289 puЫic class ApplicationDbContext : DbContext { } puЬlic ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base (options) { } puЬlic DbSet<Product> Products { get ; set; puЬlic DbSet<Order> Orders { get; set; } Такое изменение является достаточным основанием для инфраструктуры Entity Framework Core создать миграцию базы данных, которая по зволит объектам Order сохраняться в базе данных. Для создания миграции откройте консоль диспетчера па­ кетов , выбрав пункт меню ToolsqNuGet Package Maпager (Сервис qДиспетчер пакетов NuGet) , и запустите следующую команду: Add - Migration Orders Эта команда сообщает EF Core о необходимости получить новый снимок приложе­ ния, выяснить его отличия от предыдущей базы данных и сгенерировать новую миг­ рацию под названием Orders . Чтобы обновить схему базы данных, запустите такую команду: Update - Database Переустановка базы данных Если вы часто вносите изменения в модель, то столкнетесь с ситуацией, когда миграции и схема базы данных потеряют синхронизацию . Самое простое, что можно сделать - удалить базу данны х и начать заново . Однако, естественно, такой подход приемлем только на ста­ дии разработки, потому что все сохраненные данные будут утрачены . Выберите пункт SQL Server Object Explorer (Проводник объектов SQL Server) из меню View (Вид) среды Visual Studio и в от к рывшемся окне щелкните на кнопке Add SQL Server (Добавить сервер SQL) . В поле Server Name (Имя сервера) введите ( localdb) \ mssqllocaldb и щелкните на кнопке Соппесt (Подключиться). В окне проводника объектов SQL Server появится новый элемент, который можно раскрыть , чтобы увидеть созданные базы данных LocalDB. Щелкните правой кнопкой мыши на име­ ни базы данных , которую вы хотите удалить, и выберите в контекстном меню пункт Delete (Удалить). В открывшемся диалоговом окне отметьте флажок для закрытия всех существу­ ющих подключений и затем щелкните на кнопке ОК, чтобы удалить базу данных . После того , как база данных удалена, запустите в консоли диспетчера пакетов следующую команду, чтобы создать базу данны х и применить подготовленные миграции: Update - Database Эта команда переустановит базу данных, так что она в точности отразит вашу модель и позволит возвратиться к разработке приложения. Создание хранилища заказов Чтобы предоставить доступ 1< объектам Order, мы последуем тому же самому шаб­ лону, который использовался для хранилища товаров. Добавьте в папку Models файл класса по имени IOrderReposi tory. cs и определите в нем интерфейс, приведенный в листинге 10 .14 .
290 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Листинг 10.14. Содержимое файла IOrderRepository. cs из папки Models using System . Collections . Generic ; namespace SportsStore.Models { puЫic interface IOrderRepository IEnumeraЫe <Order> Orders { get; void SaveOrder(Order order) ; Для реализации интерфейса хранилища заказов добавьте в папку Models файл класса по имени EFOrderReposi tory . cs с определением , предс тавленн ым в листин­ ге 10.15. Листинг 10.15. Содержимое файла EFOrderRepository. cs из папки Models using System . Collections . Generic ; using Microsoft.EntityFrameworkCore ; using System . Linq ; namespace SportsStore . Models { puЫic class EFOrderRepos i tory : I OrderRepository { private ApplicationDbContext context ; puЬlic EFOrderRepository(App licationDbContext ctx) context = ctx ; puЫic IEnumeraЫe<Order> Orders => context . Orders . Include(o => o.Lines) . Theninclude(l => l.Product); puЬlic void SaveOrder(Order order) { context . AttachRange(order . Lines.Select(l => l.Product) ) ; if (order.OrderID ==О) { context.Orders.Add(order) ; context.SaveChanges(); Класс EFOrderReposi tory реализует интерфейс IOrderReposi tory с примене­ нием Entity Framework Core. по з воляя извлекать набор сохраненных объект ов Order и создавать либо изменять заказы. Особенности хранилища заказов Реализация хранилища для заказов в листинге 10 .15 требует небольшой дополнительной работы . Инфраструктуру Eпtity Framework Core необходимо проинструктировать о загрузке связанных данных, если они охватывают несколько таблиц. В листинге 10 .15 с помощью ме­ тодов Incl ude () и Thenlnclude () указано, что когда объект Order читается из базы данных, то также должна загружаться коллекция, ассоциированная со свойством Lines, наряду с объектами Product, связанными с элементами коллекции:
Глава 1О . SportsStore : завершение nостроения корзины для nокуnок 291 puЫic IEnumeraЫe<Order> Ord e r s => co nt ext . Orders . Include (o => o . Lines} .T heninclude (l => l . Produ ct} ; Это гарантирует получение всех объектов данных, которые нужны, не выполняя запросы и не собирая данные напрямую. Дополнительный шаг требуется та кже и при сохранении объекта Order в базе данны х . Когда данные корзины пользователя десериализируются из состояния сеанса, пакет JSON создает новые объе кты, не известные инфраструктуре Eпtity Framework Core , которая затем пытается записать все объекты в базу данных. В случае объектов Prod u ct это означает, что инфраструктура EF Core попытается записать объекты, которые уже были сохранены, что приведет к ошиб ке. Во избежание проблемы мы уведомляем Eпtity Framework Core о том , что объекты существуют и не должны сохраняться в базе данных до тех пор, по ка они не будут модифицированы: context .AttachRange (order.Li n es . Select(l => l . Product}} ; В результате инфраструктура EF Core не будет пытаться записывать десериализированные объекты Product , которые ассоциированы с объектом Order . Затем хранилище заказов регистрируется как служба в методе ConfigureServices (} кла сса Startup (ли стинг 10. 16) . Листинг 10.16. Регистрация службы хранилища заказов в файле Startup. cs puЬlic void ConfigureServi ces(IServiceCol lection services} services . AddDbContext<App l icat i onDbContext>(option s => options . UseSqlSe r ver( Config u ration[" Data : Spo rtStoreProduct s : Conne ctionStri ng"J) } ; services . AddTrans i ent<IProdu ctRepos i tor y , EFP roduct Rep osito r y>(} ; services . AddScoped<Cart>(sp => SessionCart.GetCart(sp}) ; services . AddSing l eton< I HttpContextAcce s sor , HttpContextAccessor>(} ; services.AddTransient<IOrderRepository, EFOrderRepository>(); services . AddMvc(} ; services . AddMemoryCache() ; services . AddSession(} ; Зав ерше н и е построени я контроллера Order Для завершения класса Ord e r Con tro l ler понадобится модифицировать конс ­ труктор так, чтобы он получал службы, требующиеся ему для обработки заказа, и добавить новый м етод действия, который будет обрабатывать НТГР-запрос POST формы , когда пользователь щелкает на кнопке Complete order (Завершить заказ). Оба изменения показаны в листинге l О. l 7. Метод дей ствия Checkout (} декорирован атрибутом Ht tpPost, т. е. он будет вы­ зываться для запроса POS T - в это м случае, когда пользователь отправля ет форму. Мы снова полагаемся на систему привязки моделей, так что можно получить объект Order , дополнить е го данными из объекта Ca rt и сохранить в хранилище .
292 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC Листинг 10.17. За вершение контроллера в файле OrderController. cs using Microsoft . AspNetCore . Mvc ; using Spo rtsStore . Models ; using System. Linq; n a mespace SportsStore . Control l ers puЫic class OrderController : Contro lle r private IOrderRepository repository; private Cart cart; puЫic OrderController(IOrderRepository repoService, Cart cartService) { repository = repoService; cart = cartService; pu Ы i c ViewResult Checkout() = > Vi ew( n ew Order()) ; [HttpPost] puЫic IActionResult Checkout(Order order) { if (cart.Lines . Count() == 0) { ModelState.AddМodelError("", "Sorry, your cart is empty!"); if (ModelState.IsValid) { order . Lines = cart.Lines.ToArray(); repository.SaveOrder(order); return RedirectToAction(nameof(Completed)); else { return View(order); puЫic ViewResult Completed () cart.Cl.ear(); return View () ; Инфраструктура МVС контролирует огран ич ения пров ерки достов ерности, кото­ рые были прим ен ены I< классу Order посредством атрибутов аннотаций данных , и через свойство ModelState сообщает м етоду действия о любых пробл емах . Чтобы вы ­ яснить , есть ли проблемы, мы проверяем свойство ModelState . IsValid. Мы вызы­ ваем метод Mode l State. AddModelError () для регистрации сообщения об ошибке, если в корзине н ет элементов. Вскоре мы объясним , как отобр ажать такие сообщения об ошибках , а более подробное описание привязки модел ей и проверки достоверности будет представлено в главах 27 и 28. Модульное тестирование: обработка заказа Чтобы выполнить модульное тестирование класса Or derContro l ler, необходимо прове­ рить поведение версии POST метода Ch eckout () . Хотя этот метод выглядит коротким и простым, использование привязки моделей MVC означает наличие многих вещей, происхо­ дящих "за кулисами", которые должны быть протестированы .
Глав а 1О. Spo rtsStore: завершение построения к орзины для по к упо к 293 Мы хотим обрабатывать заказ , только если в корзине присутствуют элементы , и поль­ зователь предоставил достоверные детали о доставке . При любы х дру гих обстоятельс ­ твах пользователю дол жн о быть сообщено об ошибке . Вот первый тестовый метод, кото ­ ры й определен в фа й ле класса по имени OrderControllerTests . cs внутри проекта SportsStore . Tests: using Microsoft . AspNetCore . Mvc ; using Moq; using SportsStore . Controllers; using SportsSto re.Models; using Xuni t ; namespace SportsStore.Tests puЬlic class OrderControllerTests [Fact] puЬlic void Cannot_ Checkou t_Empty_ Cart() 11 Организация - создание имитированн ого хранилища заказов Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ; 11 Организация - создание пустой корзины Cart cart = new Cart () ; 11 Организация - создание заказа Order order = new Order() ; 11 Организация - создание экземпля р а контроллера OrderController target = new OrderController(mock.Object , cart) ; 11 Действие ViewResult result = target . Checkout(order) as ViewResult ; 11 Утверждение - проверка , что заказ не был сохранен mock . Verify(m => m. SaveOrder(It .I sAny<Order>()) , Times . Never); 11 Утверждение - проверка , что метод возвращает стандартное 11 представление Asser t . True(string.IsNullOrEmpty(result . ViewName)) ; 11 Утверждение - проверка , что представлению передана 11 недопустимая модель Assert . False(result . ViewData . ModelState . IsValid) ; Тест проверяет отсутствие возможности перехода к оплате при пустой корзине. Мы удосто­ веряе м ся , что метод SaveOrder () и митированной реализации IOrderRepos i tory н и ­ когда не вызывается, что метод возвращает стандартное представлен ие ( которое повторно отобразит введенные пользователем данные, давая ему шанс откорре ктировать их) и что состояние модели , передаваемое представлению, помечено ка к недопустимое. Это мо жет выглядеть как и злишн е ограничивающий набор утверждений, но для провер ки правильности поведения нуж ны все три утверждения. Следующий тестовый ме тод работает в основном так же , но внедряет в модель представления ошибку, эмулирующую проблему, о которой сообщает средство привяз ки модели (что должно происходить в производственной среде, когда пользователь вводит не корректные данные о достав ке):
294 Часть 1. Введение в и нфрастру ктуру ASP.NEТ Сог е MVC [Fact ] puЫic void Cannot_ Checkout Invalid_ShippingDetails() { //Организация - создание имитированного хранилища заказов Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ; / / Организация - создание корзины с одним элементом Cart cart = new Cart() ; cart . Additem(new Product() , 1) ; //Организация - создание экземпляра контроллера OrderControlle r target = new OrderController(mock . Object , cart) ; //Организация - добавление ошибки в мо д ель target.ModelState . AddModelError( " error ", "error"); // Действие - попытка перехода к оплате ViewResult result = target.Checkout(new Order()) as ViewResult ; 11 Утверждение - проверка , что заказ не был сохранен mock . Verify(m => m . SaveOrder(It . IsAny<Order>() ) , Times . Never) ; //Утверждение - проверка , что метод возвращает стандартное //представление Assert . True(string . IsNullOrEmpty(result . ViewName)) ; //Утверждение - проверка , что представлению передается //не д опустимая модель Assert . False(result.ViewData . ModelState . IsValid) ; Удостоверившись в том, что пустая корзина или некорректные данные о дост а в ке предо­ твращают сохранение за каза , необходимо проверить , что норм альные за казы со храняются дол жным образом . Ни же приведен тест. [Fact] puЫic void Can Checkout_And_S u bmit_Order() { //Организация - создание имитированного хранилища заказов Mock<IOrderRepository> mock = new Mock<IOrderRepository>() ; //Организация - создание корзины с одним элементом Cart cart = new Cart() ; cart.Additem(new Product() , 1) ; //Организация - соз д ание экземпляра контроллера OrderController target = new OrderController(mock . Object , cart) ; // Действие - попытка перехода к оплате RedirectToActionResult result = target.Checkout(new Order()) as RedirectToActionResult ; //Утверждение - проверка , что заказ был сохранен mock . Verify( m => m . SaveOrder(It . IsAny<Order>()) , Times . Once) ; //Утверждение - проверка , что ме т од перенаправляется на действие Completed Assert . Equal( " Completed ", result . ActionName) ; Тестировать возможность идентификации допустимы х сведений о достав ке не нужно . Это автоматичес ки обрабатывается средством привязки моделей с использованием атрибутов , примененны х к свойствам класса Order .
Глава 1О . SportsStore: завершение построения корзины для покупок 295 Отображен ие сообщений об ошибках проверки достоверности Для пров ерки пользовательских данных инфраструктура МVС будет использовать атрибуты проверки достов ерности, примененные к классу Order . Тем не мен е е , чтобы отобразить сообщения о проблемах, понадобится внести небольшое изменение. Здесь задействован еще один встро енный дескрипторный вспомогательный класс, который инспектирует со стояние проверки достоверности данных, предоставленных пользова­ телем, и добавляет предупреждающие сообщения для каждой обнаруженной проблемы. В листинге 10.18 демонстрируется добавление НТМL-элемента, который будет обраба­ тываться этим де скрипторным вспомогательным классом, в файл Chec kout . csh tml . Листинг 1О.1 В. Добавление области с итогами проверки достоверности в файле Checkout. cshtml @model Or der <h2>Check out n o w< /h2> <p>Please enter your detail s , and we 'll shi p your goods right away!</p> <div asp-validation-summary="All" class="text-danger"></div> <form asp - action= " Checkout " method= " po s t " > <hЗ>Sh ip to</hЗ> Благодаря такому простому изменению пользователю отображаются сообщения об ошибках проверки достоверности. Чтобы увидеть результат, посетите URL вида /Order/Checkout и попробуйте перейти к оплате , не выбрав ни одного товара или не указав сведения о доставке (рис. 10.5). Дескрипторный вспомогательный класс, генерирующий такие сообщения, является частью системы проверки достоверности моделей, которая подробно рассматривается в главе 27. D SportsStore Х ~ • С l CD lo~a lh ~t:бOOOOjQ~d;;/o~-::~-~~ '----=-~====-==--~ SPOH ГS STOf~Г:: . . а Ноте Chess Soccer Wa terspo rts Check ou t now Pl ease ente r your details, and we'l l ship your goods right aYJay! Please enter а name Please enterthe first addressline Pleaso ent er а с~у name Plaase eoter а stale паmе Please er1t er а country name Sorry, your cart is empty 1 Ship to Nam e: Рис. 10.5. Отображение сообщений об ошибках проверки достоверности
296 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Совет. Данные, отправленные пользователем, перед проверкой посылаются серверу, что известно как проверка достоверности на стороне сервера, для которой инфраструктура MVC предлагает великолепную поддержку. Проблема с проверкой достоверности на сто­ роне сервера заключается в том, что пользователю сообщается об ошибках лишь после того, как данные отправлены серверу и обработаны, а также сгенерирована результирую­ щая страница - на занятом сервере все это может занять несколько секунд . По указан­ ной причине проверка достоверности на стороне сервера обычно дополняется провер­ кой достоверности на стороне клиента, при которой введенные пользователем значения проверяются с помощью кода JavaScript до отправки серверу данных формы . Проверка достоверности на стороне клиента будет описана в главе 27. Отображение итоговой страницы Чтобы завершить реализацию процесса оплаты, необходимо создать представ­ ление, которое будет отображаться, когда браузер перенаправляется на действие Compl eted контроллера Order. Добавьте в папку Vi e ws /Order файл представления Razor по имени Comp l eted . c s h tml и поместите в него разметку . приведенную в лис­ тинге 10.19. Листинг 10.19. Содержимое файла Completed. cshtml из папки Views/Order <h2>Thanks !</h2> <p>Thanks for placing you r o rder . </p> <p>We'll ship your goods as soon as possiЫe.</p> Для интеграции этого представления в приложение никаких изменений в код вно­ сить не придется, пос1юльку требуемые операторы уже были добавлены при опреде­ лении метода действия Com p leted () в листинге 10.17. Теперь пользователь может проходить через весь процесс, начиная с выбора товаров и заканчивая переходом к оплате. При условии, что пользователь предоставил корректные сведения о доставке (и в корзине есть какие-то товары), после щелчка на кнопке Complete order он увидит итоговую страницу (рис . 10.6). 1 1 Spor1sStore ' Ord •r Subm Х ~ -> С ·15 i;;~~lh~~tб0000/ord;,:/Coinpt;t;d·---·-------·--·-------·----·- ~ Е SPOATS STORE . . [~] . Hom e Th anks! Ch ess Thanks fог placing your огdег. Soccer We'll ship уош goods as soon as possiЫe. 1 Watersp orts l_______________________________________J Рис. 10.6. Итоговая страница завершенного заказа
Глава 1О. SportsStore: завершение построения корзины для пок у пок 297 Резюме Мы завершили все основные части приложения SportsStore , отвечающие за вза­ имодействие с пользователями. Конечно, до сайта Aшazon приложению далеко. но в нем имеется каталог товаров с возможностью просмотра по категориям и страницам, аккуратная корзина для покупок и простой процесс оплаты. Архитектура с хорошим разделением означает, что мы можем легко изменять поведение любой порции приложения, не беспокоясь о возникновении проблем или несовместимости где-либо в приложении. Например, мы могли бы изменить способ сохранения заказов, и это никак бы не повлияло на 1<0рзину для покупок, каталог то ­ варов или любую другую область приложения. В следующей главе мы добавим средс­ тва, необходимые для администрирования приложения SportsStore .
ГЛАВА 11 SportsStore: администрирование в настоящей главе мы продолжим построение приложения SportsStore, чтобы предоставить администратору сайта способ управления заказами и товарами. Управление заказами В предыдущей главе была добавлена поддержка для получения заказов от пользо­ вателей и сохранения их в базе данных. В этой главе мы собираемся создать простой инструмент администрирования, который позволит просматривать получ енные зака­ зы и помечать их как отгруженные. Расширение модели Первым изменением, которое необходимо внести, является расширение модели , чтобы можно было фиксировать, какие заказы были отгружены. В листинге 11.1 показано добавление нового свойства в класс Order , который опред елен в файл е Order. cs внутри папки Models. Листинг 11.1. Добавление свойства в файле Order. cs using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft .AspNetCore . Mvc . ModelBinding ; namespace SportsStore . Models puЫic class Order { [BindNever] puЫic int OrderID { get ; set ; } [BindNever] puЫic I Col lection <CartLine> Lines { get ; set; } [BindNever] puЬlic bool Shipped { get; set; } [Required(ErrorMessage = " Please enter а name " )] / / Введите имя puЫic string Name { get; set; ) [Required(ErrorMessage = " Please enter the first address line")] 11 Введите пер вую строку адреса
puЬlic string Line l puЫ i c string Line2 puЫic stri ng LineЗ get; set; get; set; get; set; Глава 11 . SportsStore: администрирование 299 [Required(ErrorMessage = "Please enter а city name " ) ] 11 Вв едите название г орода puЫic string City { get ; set; } [Required (ErrorMessage = "Plea se e n ter а state name")] 11 Вве дите наз вание штата puЫic string State { get ; s et; } puЫic string Zip { get; set; } [Required(ErrorMessage = "Ple ase ente r а country name")] 11 Вв едит е название страны puЫic string Country { get ; s et ; } рuЫic bool GiftWrap { get; set; } Такой итеративный подход к расширению и приспосабливанию модели для под­ держки ра зличных средств типичен при разработке приложений МVС. В идеальном мире у нас была бы возможность полностью определить классы моделей в начале прое1па и просто строить приложение на их основе, но так случается только в про­ стейших проектах, а на практике следует ожидать итеративную разработку по мере понимания того , что требуется разрабатывать и развивать . Миграции Entity Framework Core облегчают этот процесс , поскольку нам не при­ ходится вручную удерживать схему базы данных в синхронизированном состоянии с кл ассами моделей, создавая и запуская команды SQL. Чтобы обновить базу данных для отражения свойства Shipped , добавленного в класс Order, откройте консоль дис­ петчера пакетов и запустите следующие команды, которые создадут новую миграцию и применят ее к базе данных: Add- Migration Shipped Orders Opdate - Database Добавление действий и представления Функциональность, требуемая для отображения и обновления набора заказов в базе данных, относительно проста, потому что она строится на основе средств и ин­ фраструктуры. которые бьши созданы в предшествующих главах. В листинге 11 .2 к контроллеру Order добавляются два метода действий. Листинг 11.2 . Добавление двух методов действий в файле OrderController. cs using Microsoft . AspNetCo re. Mv c ; using SportsStore . Model s; using System . Linq; namespace SportsStore . Controllers puЫic class OrderControll er : Controller private IOrderRepository repository ; private Cart cart ;
300 Часть 1. Введение в инфраструктуру ASP.NET Core MVC puЫic OrderController(IOrderRepository repoService , Cart cartService) { repository = repoService ; cart = cartService ; puЬlic ViewResult List () => View(repository.Orders.Where(o => !o.Shipped)); [HttpPost] puЬlic IActionResult MarkShipped (int orderID) { Order order = repository.Orders .F irstOrDefault(o => o.OrderID if (order != null) { order.Shipped = true; repository.SaveOrder(order); orderID) ; return RedirectToAction(narneof(List)); puЫic ViewResult Checkout() => View(new Order()) ; [HttpPost] puЫic IActionResult Checkout(Order order) { if (cart.Lines . Count() ==О) { ModelState . AddModelError( "", " Sorry , your cart is empty!"); if (ModelState . IsValid) { order . Lines = cart . Lines . ToArray() ; repository . SaveOrder(order) ; return RedirectToAction(nameof(Completed)) ; else { return View(order) ; puЫic ViewResult Completed() { cart . Clear() ; return View () ; Метод List () выбирает все объекты Order в хранилище, свойство Shipped кото­ рых имеет значение false , и передает их стандартному представлению . Этот метод действия будет использоваться для отображения администратору списиа неотгружен­ ных заказов. Метод MarkShipped () будет получать запрос POST, указывающий идентифика­ тор заказа, который применяется для извлечения соответствующего объекта Order из хранилища, чтобы установить его свойство Shipped в true и сохранить. Для отображения списка неотгруженных зюtазов добавьте в папку Views/Order файл представл ения Razor по имени List . cshtml и поместит е в него разм ет1<у из листинга 11.3 . Элемент tаЫе используется для отображения ряда деталей, включая сведения о приобретенных товарах.
Глава 11 . SportsStore : администрирование 301 Листинг 11.З. Содержимое файла List. cshtml из папки Views/Order @mode l IEnumeraЫe<Order> @{ } ViewBag.Title = " Orders "; Layout = "_AdminLayout "; @if (Model . Count() > 0) { <tаЫе class= " taЫe taЫe - bordered taЫe-striped" > <tr><t h >Name</th><th>Zip</th><t h colspan=" 2 " >Detail s</th><t h ></th></ tr> @foreach (Order о in Model) { <tr> <td>@o . Name</td><td>@o . Zip</td><th>Product</th><th>Quantity</th> <td> . <form asp - ac tion="Ma rkS hipped " method="post " > <i nput type= "hidden " name ="orderid" value= " @o.OrderID " /> <button type= " s ubmit" class= "btn btn - sm btn -danger " > Ship </button> </form> </td> </tr> @foreach (CartLine line in o . Li nes) { <tr> <td colspan= " 2 "></t d> <td>@line . Product . Name</td><td>@l ine. Quantity</td> <td></td> </tr> </tаЫе> else { <div class= " text - center " >No Unshipped Orders</di v> Каждый заказ отображается с кнопкой Ship (Отгрузить) , которая отправляет фор­ му методу действия MarkShipped ().С помощью свойства Layout для пр едставления List указана другая компоновка, которая переопределяет компоновку, заданную в файле_ViewStart . cshtml . Для добавле ния компоновки создайте в папке Views/Share d файл по имени _AdminLayout . cshtml с при м ен ением шаб л она элемента MVC View Layout Page (Страница компоновки представления MVC) и поместите в него разметку, показан­ ную в листинге 11.4 . Листинг 11.4 . Содержимое файла _ AdminLayout. csh tml из папки Views / Shared < ! DOCTYPE html > <html> <head> <meta name= " viewport " content= " width=device - width" /> <link rel= " stylesheet " asp -hr ef -in clude= "li Ь/bootstrap/dist/css/* . min.css " /> <title>@ViewBag . Title</title> </head>
302 Часть 1. Введение в инфраструктуру ASP.NET Core MVC <body class="panel panel-default " > <div class= " panel-heading"><h4>@ViewBag.Title</h4></div> <div class= " panel-body " > @RenderBody () </div> </body> </html> Чтобы просматривать и управлять заказами в приложении, запустите приложение, выберите некоторые товары и перейдите к оплате. Затем посетите URL вида /Order / List. Вы увидите сводку по созданным заказам (рис. 11.1). Щелкните на кнопке Ship; база данных обновится, а список ожидающих заказов будет пуст. ~ Ord< rs Х f- · С 1ф localhost.60000/ 0rder/list - -;-1:1 1--~~~..:=====~=-==:::.:::..:::..:::...=-~-==-7--=========-7-.=::::.:::.:::::::::::.:::...:=:..:::о....=:::_:::::_1...:._ , 1 1 Orders 1 1 Name Zip Details Alice 10036 Product Quantity • 1 1 Lifejacket Socce r Вall l______ - ----- ""______" _ _ __ -·---- ",_"____ ·-·--- -- - ·- -- J Рис. 11.1. Управление заказами На заметку! В настоящий момент ничего не может воспрепятствовать запросу пользовате­ лем URL вида /Order/List и администрированию своего заказа. В главе 12 объясня­ ется, как ограничивать доступ к методам действий. Добавление средств управления каталогом Соглашение для управления более сложными коллекциями элементов предусмат­ ривает предоставление пользователю страниц двух типов: страницы списка и страни­ цы редактирования (рис. 11.2). Вместе эти страницы позволяют пользователю создавать, читать, обновлять и уда­ лять (create, read, update, delete - CRUD) элементы в коллекции. Такие действия на­ зываются операциями CRUD. Разработчики нуждаются в реализации операций -СRUD настолько часто, что средство формирования шаблонов в Visual Studio предлагает сценарии для создания контроллеров CRUD с заранее определенными методами дейс­ твий (вЮiючение средства формирования шаблонов рассматривалось в главе 8) . Но, как и со всеми шаблонами Visual Studio, я считаю, что изучать возможности ASP.NET Core MVC лучше напрямую.
List Screen ltem Kayak Lifejacket Soccer ball Actions Edit 1Delete Edit 1Delete Edit 1Delete Add New ltem Глава 11. SportsStore: администрирование 303 Edit ltem: Kayak Name: 1 Kayak :=::=====::::::: Description: 1 А boat for one ре. " Category: Watersports Price ($): 275 .00 Save Cance\ Рис. 11.2 . Эскиз пользовательского интерфейса CRUD для каталога товаров Создание контроллера CRUD Начнем с создания отдельного контролле ра для управления каталогом товаров. Добавьте в папку Controllers файл класса по имени AdminController . cs с кодом, прив еденны м в листинге 11 .5. Листинг 11.5. Содержимое файла AdminController. cs из папки Controllers using Microsoft . AspNetCore.Mvc ; using SportsStore . Models ; namespace SportsStore.Controllers puЫic class AdminController : Controller private IProductRepository repository ; puЫic AdminController(IProductRepository repo) { repository = repo; puЬlic ViewResult Index() => View(repository . Products) ; В конструкторе контроллера объявлена зависимость от интерфейса IProductReposi tory, которая будет распознаваться при создании экземпляров. В классе контроллера определен единственный метод действия Index {), который вызывает метод View () , чтобы выбрать стандартное представление для действия, и передает ему в качестве модели представления набор товаров из базы данных. М одульное тестирование: метод действия Index () Нас интересует поведение метода действия Index () в к онтроллере Admin , которое за­ ключается в корректном возвращении объектов Product из хранилища. Протестировать это можно за счет создания имитированной реализации хранилища и сравнения тестовых данных с данными, которые возвращает метод действия. Ниже показан код модульного теста , помещенный в новый файл по имени AdminControllerTests. cs внутри проекта SportsStore . Tests.
304 Часть 1. Введен и е в инфрастру к туру ASP.NET Со ге MVC using System . Collections . Generic ; using System.Linq ; using Microsoft . AspNetCore . Mvc; using Moq; using SportsStore.Controllers ; using SportsStore . Models; using Xunit ; namespace SportsStore.Tests puЬlic class AdminControllerTests [Fact] puЫic void Index_Contains_All Products() { //Организация - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m . Products) .Returns(new Product[] { new Product {ProductID 1 , Name "Pl"}, new Product {ProductID new Product {ProductID )); 2, Name 3, Name "Р2"}, "РЗ"}, //Организация - создание контроллера AdminController target = new AdminController(mock . Object) ; 11 Действие Product [ J result = GetViewModel<IEnumeraЫe<Product>>(target . Index())? . ToArray() ; //Утверждение Assert . Equal(З , result.Length) ; Assert.Equal( " Pl " , result [ OJ . Name) ; Assert . Equal( " P2 " , result[l] . Name) ; Assert.Equal( " PЗ ", result[2 ] . Name) ; private Т GetViewModel<T>(IActionResult result) where Т return (result as ViewResu l t)?.ViewData.Model as Т; class { В тест был добавлен метод GetViewModel () для расп аков ки результата, возвращаемого методом де йствия, и получен и я данных модели представления . Далее в глав е будут реали­ зованы дополнительные тесты , к оторые используют этот метод . Реализация представления списка Следую щим шаг ом будет добавление пр едставления для метода действия Index ( ) контроллера Admin. С оздайте пап ку Views/Admin и доб авьте в нее файл п редставле ­ ния Razor по имени Index . cshtml с содержимым, приведенным в листинге 11 .6 .
Глава 11 . SportsStore : администрирование 305 Листинг 11.6 . Содержимое файла Index. cshtml из папки Views/Admin @model IEnumeraЫe<Product> @{ ViewBag . Title = "All Produ cts "; Layout = " _Adm inLayout"; <tаЫе class= " taЫe taЫ e -strip ed taЬle-bordered taЬle-condensed " > <tr> <th class= "text - right">ID</th> <th>Name</th> <th c l ass= "text - right " >Price</th > <th class= " text - ce n ter " >Actions</th > </tr> @foreach (var item in Model) { <tr> <td class="text-right " >@ item.ProductID< /td> <td>@item . Name</td> <td class= " text - right " >@item .Price .T oStr ing (" c ") </td> <td cl ass="text-center"> <form asp- action= " De l ete " method="post " > <а asp-action= " Edit" class= " btn btn-sm btn -w arni n g " asp - route - productid= " @item .Produ ct I D" > Edit </а> <input type= " hidden " name= " ProductID " value= " @item .Produ ctID " /> <button type= "s ubmit " class= "btn btn- danger btn- sm"> Delete </button> </form> </td> </tr> </tаЫе> <div class= " text- cen ter" > <а asp - act i on= " Create " class= "Ьt n Ьtn - primary " >Add Product</a> </div> Представление содержит таблицу, в ноторой для наждого товара предусмотрена строна с ячейками, содержащими наименование и цену товара. Кроме того, в каждой строке присутствуют I<нопки , которые по зволят редактировать сведения о товаре и удалять его, отправляя запросы к действиям Edi t и Delete . В дополнение к таблице имеется кнопка Add Product (Добавить товар), нацеленная на действие Crea te. Мы добавим действия Edit, Delete и Create в по следующих разделах, а пок а можно посмотреть, как отображаются товары, запустив приложение и запросив URL вида / Admin/ Index (рис. 11.3) . Совет. Кнопка Edit (Редактировать) находится внутри элемента form в листинге 11.6, так что две кнопки располагаются рядом благодаря примененному интервалу Bootstrap . Кнопка Edit будет посылать серверу НТТР-запрос GET для получения текущих сведений о товаре; это не требует элемента form. Однако поскольку кнопка Delete (Удалить) будет вносить изменение в состояние приложения , необходимо использовать НТТР-запрос POST , кото­ рый требует элемента form.
306 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC D All Produc ts Х j_:_ ~--С [~ loca~os~OOOO/Adm:~/ln~~- All Prod ucts ID Name 1 Kayak 2 l ilejackel 3 Soccer ВaJI 4 Corne r Flags 5 S\adium 6 Т11inking Сар 7 Unsleady Chair 8 Huma n Cl1ess Boa rd 9 Bling..Bling Кing Price $275.00 $48.95 $19.50 $34 .95 579.500 .00 $16.00 $29.95 $75.00 $1,200 .00 Aclions " 111 ." ,,. " Е181 B lll -· 1111 131111 "' 111 111 11 1ФФtil ___. _______ j 1 [__·-·------ Рис. 11.3 . Отображение списка товаров Редактирование сведений о товарах Чтобы предоставить средства создания и обновления, мы добавим страницу ре­ дактирования сведений о товаре, подобную показанной на рис. 11.2. Задача состоит из двух частей: • отображение страницы, которая позволит администратору изменять значения для свойств товара; • добавление метода действия, который обработает внесенные и зменения, когда они будут отправлены. Соэдание метода действия Edi t (; В листинге 11 .7 приведен код метода действия Edit ( ) , добавленного в контроллер Adrn i n , который будет получать Н1ТР-запрос, отправляемый браузером, когда пользо­ ватель щелкает на кнопке Edit . Листинг 11.7 . Добавление метода действия Edi t () в файле AdminController. cs using Microsoft .AspNetCore. Mvc ; using Spor t sStore. Mode ls; using System.Linq;
Глава 11 . SportsStore : администрирование 307 namespace SportsStore . Controllers { puЬlic cl ass AdminCont r ol l er : Control ler private I ProductReposi tor y r epositor y ; puЫic AdminController(IP r od u ctRepository repo) { repository = repo ; puЫic ViewResult I n dex() = > View(repository . Produc t s) ; puЫic ViewResult Edit(int productid) => View(repository.Products .FirstOrDefault(p => p.ProductID == productid)); Этот простой метод ищет товар с идентификатором, соответствующим знач е ­ нию параметра productid, и передает его как объект модели представления методу View() . М одульное тестирование: метод действия Edi t () В методе действия Edi t () нам необходимо протестировать две линии поведения. Первая заключается в том, что мы получаем запрашиваемый товар, когда предоставляем допусти­ мое значение идентификатора, чтобы удостовериться в редактировании ожидаемого това­ ра. Вторая проверяемая линия поведения связана с тем, что мы не должны получать товар при запросе значения идентификатора, отсутствующего в хранилище. Ниже показаны тес­ товые методы, добавленные в файл класса AdminControl l erTests . cs. [Fact] puЬlic void Can_Ed i t Prod uct( ) { 11 Организация - создание имитирова нн о г о храни л ища Mock<IProductRepository> mock = new Mock<IProduc t Repository>() ; moc k . Setup(m => m.Products) .Returns(n e w Prod u ct [ ] { new Product {ProductID 1, Na me "Pl"), new Product {ProductID 2, Name "Р2"), new Product {ProductI D З , Name " РЗ "}, }); 11 Организация - созда ние контроллера AdminC o ntrol l er target = n ew Admi n Contro ll er(mock . Obj ect ); 11 Действие Pro duct pl Product р2 = Product рЗ = GetViewMode l < P roduct>(target . Edit(l)) ; GetV i ewMod el < P roduct>(target . Edit(2)) ; GetViewMode l <Product>(ta rg e t.E dit(З) ); 11 Утверждение Assert . Equal(l , pl.Produ ctI D) ; Assert . Equal(2 , p2 . Produ ctID) ; Assert. Equal(З, pЗ . ProductID) ; [Fact ] puЬlic void Cann ot_Edit_Nonexistent_ Product( ) { 11 Организация - созда ни е ими т ирован н о г о хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>() ;
308 Часть 1. Введение в инфраструктуру ASP.NET Core MVC mock . Setup(m => m.Products) . Returns(new Product[] new Product (ProductID 1, Name "Pl"}, new Product {ProductID 2 , Name " Р2 " ) , new Product ( ProductID 3, Name "РЗ"}, }); 11 Организация - создание контроллера AdminController target = new AdminController(mock.Object) ; 11 Действие Product result = GetViewModel<Product>(target.Edit(4)) ; //Утверждение Assert . Null(result); Создание представления редактирования Теперь, располагая методом действия, можно создать представление для отоб­ ражения . Добав ьте в папку Views/Admin файл представления Ra zor по имени Edi t . cshtrnl и поместите в него разметку, приведенную в листинге 11.8 . Листинг 11.8 . Содержимое файла Edit. cshtml из папки Views/Admin @model Product @{ ViewBag.Title = " Edit Product" ; Layout = "_AdminLayout "; <form asp - action="Edit " method= "p ost " > <input type= " hidden " asp-for= " ProductID " /> <div class="form- group " > <label asp-for= " Name " ></label> <input asp - for= " Name " class= " form - control " /> </div> <div class= " form - group " > <label asp - for="Description " ></label> <textarea asp - for="Description " class= " form-control"></textarea> </div> <div class="form- group " > <label asp - for= " Category"></label> <input asp - for= " Category" class= " forrn - control " /> </div> <div class= " form - group"> <label asp - for="Price " ></label> <input asp - for= " Price " class= " form - control" /> </div> <div class="text - center " > <button class="btn btn - primary" type="submit " >Save</button> <а asp - action= "I ndex " class="btn btn - default">Cancel</a> </div> </form>
Глава 11. SportsStore: администрирование 309 В представлении имеется форма HTML, большая часть содержимого которой гене­ рируется посредством дескрипторньrх вспомогательных классов, включая установку целей для элементов form и а, устано вку содержимого элементов label и выдачу ат­ рибутов name, id и value для элементов input и textarea . Чтобы увидеть НТМL-разметку, генерируемую представлением, запустите прило­ жение, перейдите на URL типа /Adrnin/Index и щелкните на кнопке Edit для одного из товаров (рис. 11.4). х 1f- 1- С i ф lo;alhostбO·JOO/P.dm111/Edit?productld=3 i Edit Product 1 j Name 1 Soc.cer Ball 1 ' i i 1 j_ Descri pti on FIFA -a pp ro ved size and wei ghl Category Soccer Price 19.50 - Cancel /, 1 1 ·- - ----- ---- ---- _1 Рис. 11.4 . Отображение сведений о товаре для редактирования Совет. Скрытый элемент input для свойства ProductID применяется ради простоты. Значение ProductID генерируется базой данных как первичный ключ, когда новый объ­ ект сохраняется инфраструктурой Eпtity Framework Саге, и его безопасное изменение мо­ жет оказаться сложным процессом. Обновление хранилища товаров Пр ежде чем можно будет обрабатывать результаты редактирования, хранили­ ще товаров понадобится расширить, добавив возможность сохранения изменений. Первым делом необходимо добавить к интерфейсу IProductRepository новый ме­ тод (листинг 11 .9).
310 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 11.9. Добавление метода в файл IProductResposi tory. cs using System . Collections . Generi c ; namespace SportsStore . Models { puЫic interface IProductRepository IEnumeraЫe < Product> Products { get ; void SaveProduct(Product product); Затем к реализации хранилища с помощью Entity Framework Core , которая определена в файле EFProductReposi tory. cs, можно добавить новый метод (листинг 11.10) . Листинг 11.1 О. Реализация метода SaveProduct () в файле EFProductReposi tory. cs using System.Collections . Generic ; using System.Linq; namespace SportsStore . Models { puЬlic class EFProductRepository : IProductRepository { private App licationDbContext context ; puЫic EFProductRepository(ApplicationDbContext ctx) { context = ctx ; puЫic IEnumeraЫe<Product> Products => context.Products ; puЫic void SaveProduct(Product product) if (product.ProductID ==О) { context.Products.Add(product); else { Product dЬEntry = context.Products .FirstOrDefault(p => p.ProductID == product.ProductID); if (dЬEntry != null) { dЬEntry.Name = product.Name; dЬEntry.Description = product.Description; dЬEntry.Price = product.Price; dЬEntry.Category = product.Category; context.SaveChanges(); Реализация метода SaveChanges () добавляет товар в хранилище, если значение Product I D равно О ; в противном случае применяются изменения к существующей записи в базе данных. Мы не хотим здесь вдаваться в детали инфраструктуры Entity Framework Core, пос­ кольку, как упоминалось ранее , это отдельная крупная тема, к тому же она не является частью ASP.NET Core МVС. Тем не менее, в методе SaveProduct () есть кое-что, что оказьmает влияние на проектное решение, положенное в основу приложения МVС.
Глава 11. SportsStore : администрирование 311 Нам известно, что обновление должно выполняться, когда получен параметр Product, который имеет ненуле вое значение ProductID. Это делается путем извле­ чения из хранилища объекта Product с тем же самым значением ProductID и об­ новления всех его свойств, чтобы они соответствовали значениям свойств объекта, переданного в качестве параметра. Причина таких действий в том, что инфраструктура Entity Framework Core отсле­ живает объекты, которые она создает из базы данных. Объект, переданный методу SaveChanges () , создается системой привязки моделей MVC , т.е. инфраструктура Entity Framework Core ничего не знает о новом объекте Product, и она не будет при­ менять обновление к базе данных, иогда объеит Product модифицирован. Существует множество способов решения уиазанной пробл емы, но мы принимаем самый простой из них. предполагающий поиск соответствующего объекта, о котором известно инф­ раструктуре Entity Framework Core, и его явное обновление. Добавление нового метода в интерфейс IProductRepository нарушает работу класса имитированного хранилища FakeProductReposi tory, который был создан в главе 8. Имитированное хранилище использовалось для быстрого старта процес­ са разработки и демонстрации возможности применения служб для гладкой заме­ ны реализаций интерфейса, не изменяя компоненты, которые на них опираются. Имитированное хранилище больше не понадобится. В листинге 11.11 видно, что ин­ терфейс IProductReposi tory удален из объявления класса, поэтому продолжать мо­ дификацию класса по мере добавления функций хранилища не придется. Листинг 11.11. Отсоединение класса от интерфейса в файле FakeProductRepository.cs using System.Collections . Generic ; namespace SportsStore . Models { puЬlic class FakeProductRepository /* : IProductRepository */ puЫic IEnumeraЬle<Product> Products => new List<Product> new Product { Name "Football" , Price = 25 }, new Product { Name "Surf board", Price = 179 }, new Product { Name " Running shoes", Price = 95 } }; Обработка запросов POST в методе действия Edi t () К настоящему моменту все готово для реализации в контроллере Admin перегру­ женной версии метода действия Edi t () , иоторая будет обрабатывать запросы POST , инициируемые по щелчку администратором на кнопке Save (Сохранить) . Код нового метода приведен в листинге 11.12 . Листинг 11.12. Определение версии метода действия Edi t () , обрабатывающей запросы POST, в файле AdminController. cs using Microsoft.AspNetCore .Mvc; using SportsStore . Models; using System . Linq ;
312 Часть 1. Введение в инфраструктуру ASP.NET Core MVC namespace SportsStore.Controllers { puЫic class AdminCo ntr o ller : Controller private IP roductRepository repository ; puЬlic AdminControl ler( I ProductRepository repo) { repository = repo ; puЬlic ViewResult Index() => View(repository .Prod u cts) ; puЫic ViewResult Edit(int prod uctid) => View(repository . Products . FirstOrDefault(p => p . ProductID == productid)); [HttpPost] puЫic IActionResul t Edi t (Product product) if (ModelState.IsValid) { repository.SaveProduct(product); TempData [ "message"] = $ 11 {product. Name} has been saved"; return RedirectToAction("Index"); else { 11 Что-то не так со значениями данных return View(product); Мы выясняем, смог ли процесс привязки модели проверить достоверность отправлен­ ных пользователем данных, для чего читаем значение свойства ModelState . IsValid. Если здесь все в порядке, тогда мы сохраняем изменения в хранилище и направляем пользователя на действие I ndex , таJ< что он увидит модифицированный список това­ ров. В случае какой-нибудь проблемы с данными мы снова визуализируем стандарт­ ное представление, чтобы пользователь мог внести 1юрректировки. После сохранения изменений в хранилище сообщение сохраня ется с использо­ ванием средства TempDa t а, которое является частью средства состояния сеанса ASP.NET Core. Это словарь пар "1шюч /знач ение", похожий на применяемые ранее средства данных сеанса и ViewBag . Основное отличие объекта TernpData от данных сеанса в том, что он хранится до тех пор, пока не будет прочитан. В такой ситуации использовать ViewBag невозможно, потому что объект ViewBag передает данные между контроллером и представлением, и он не может удерживать данные дольше, чем длится текущий НТТР-запрос. Когда редактирование успешно, браузер перенаправляется на новый URL, поэтому данные ViewBag утрачиваются. Мы могли бы прибегнуть к средству данных сеанса, но тогда сообщение хранилось бы вплоть до его явного удаления, чего не хотелось бы делать. Таким образом, объе1п TempData подходит ка~< нельзя лучше. Данные ограничи­ ваются сеансом одного пользователя (пользователи не видят объекты TempData друг друга) и хранятся достаточно долго, чтобы быть прочитанными . Мы будем читать данные в представлении , которое визуализируется методом действия, куда был пере­ направлен пользов атель, и определяется в следующем разделе.
Глава 11 . SportsStore: администрирование 313 М одульное тести ро вание: метод действия Edi t () , обр абаты вающий запросы POST В м етоде де йствия Edi t () , обрабатывающем запросы POST , мы дол ж ны удостоверить­ ся , что хранилищу товаров для сохранен и я передаются допустимые обновления объекта Product , полученного в качестве аргумента метода. Кроме того , необходимо проверить , что недопусти мые обновления (т.е. содержащие ошиб к и провер к и достоверности модели) в хранилище не передаются. Ниже приведены тестовые методы, которые добавлены в файл Ad.minContro l lerTests.cs . [Fact] puЫic void Can Save _ Valid_ Changes() { 11 Организация - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>() ; 11 Организация - соз д ание имитированных временных данных Mock<ITempDataDictionary> tempData = new Mock<I TempData Dictionary>() ; 11 Организация - создание контроллера AdminController target = new AdminCont r o l ler(mock . Object) { TempData = tempData . Object }; 11 Организация - создание товара Product product = new Product { Name = "T est " }; 11 Действие - попытка сохранить товар IActionResult result = target . Edit(product); 11 Утверждение - проверка того , что к хранилищу был о произведено обращение mock.Verify(m => m . SaveProduct(product)) ; 11 Утверждение - проверка , что типом р езультата является перена правление Assert . Is Type<RedirectToActionResul t>(result) ; Assert . Equal( " Index ", (result as Red i rectToActionResult) .ActionName); [Fact] puЫic void Cannot_Save _ Invalid_ Chang e s() { 11 Организация - создание имитированно г о хранилища Mock<IProductRepos i tory> mock = n ew Mock<IProductRepository>() ; 11 Организа ция - созда ние контроллера AdminController target = new AdminController(mock . Object) ; 11 Организация - создание товара Product product = new Product { Name = " Test " }; 11 Организация - добавле ние ошибки в состояние модели target . ModelState . AddModelE r ro r ( " e r ror ", "error"); 11 Действие - попытка сохранить товар IActionResult resu l t = target . Edit(prod u ct) ; 11 Утверждение - проверка того , что к хранилищу был о произведено обращение mock.Verify(m => m . SaveProduct(It.IsAny<Prod u ct>()) , Times.Never()) ; 11 Утверждение - проверка типа результата метода Assert . IsType<ViewRes u l t >(result) ;
314 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Отображение подтверждающего сообщения Мы будем иметь дело с сообщением, сохраненным с помощью Temp Data , в файле компоновки Adm inLayout . cshtml (листинг 11.13). За счет обработки сообщения в компоновке мы можем создавать сообщения в любом представлении, которое при­ меняет эту компоновку, без необходимости в создании дополнительных выражений Razor. Листинг 11.1 З. Обработка сообщения ViewBag в файле _AdminLayou t. cshtml < ! DOCTYPE html> <html> <head> <m eta name="viewport" content ="width=device-width " /> <l ink r el= " stylesheet" asp - href -in clude="liЬ/bootstrap/dist/css/*.min.css" /> <title>@ViewBag.Ti tle</ti tle> </head> <body class= " panel panel-default "> <div c l ass= " panel-heading " ><h4>@ViewBag . Title</h4></div> <d i v class= " panel-bod y"> @if (TernpData["rnessage"] != null) { <div class="alert alert-success">@TernpData [ "rnessage"] </div> @RenderBody () </div> </body > </html> Совет. Преимущество такой работы с сообщением внутри компоновки заключается в том, что пользователи будут видеть его на любой странице, визуализированной после сохра ­ нения изменений. В данный момент мы возвращаем его списку товаров , но рабочий поток можно изменить с целью визуализации какого-то другого представления , и пользовател и будут по-прежнему видеть это сообщение (при условии, что следующее представление использует ту же самую компоновку). Теперь мы располагаем всеми фрагментами для редактирования сведений о това­ рах. Чтобы увидеть, как они все работают, запустите приложение, перейдите на URL вида /Admin/Index , щелкните на кнопке Edit и внесите изменение. Затем щел кните на 1шопке Save. Произойдет перенаправление на /Admin/Index и отобразится со­ общение из TempData (рис . l l.5) . Если вы перезагрузите страницу со списком това­ ров, то сообщение исчезнет. поскольку после чтения объект TempData удаляется . Это очень удобно, т.к. не приходится иметь дело со старыми сообщениями. Добавление проверки достоверности модели Мы добрались до точки, когда к классам модели необходимо добавить правила про­ верки достоверности. Пока что администратор может ввести отрицательнь1е значения для цен или оставить описания пустыми - и приложение SportsStore благополучно сохранит эти данные в базе. Смогут ли недопустимые данные успешно сохранить­ ся, зависит от того, удовлетворяют ли они ограничениям в таблицах SQL, созданных инфраструктурой Entlty Framework Core, и для большинства прилож ений таких мер безопасности будет недостаточно .
Глава 11. SportsStore : администрирование 315 CJ Ed~ Produc< Х !'. гп- -· -·--- -· -- .--:: D All P•odu«o Х'' 1~ С 1 <D l0<:a lhos t·60000/Admin/Erl 1t< --- ·------------~l 1--- -- .==:.""'=--'=--== _.!:__:_. С 1<D_ lo~alhost600:~:Adrn:-'ln~~x _ ·- _ *.J '. Edit Product / t All Products 1 1 1 Namв 1 Gree п Kayak Green Kayak /1as Ь!!еn saved Descri tio n А t for one person 1D Name 1 Gree n Kayak 1"~ 2 Lifejac kel 3 SoccerВalf 4 Corner Flags 5 Stad ium 1 Са l_ " _ ·----- ----·-------~ 6 ThinkingCap 7 Unstaady Chair в Human Ch ess Вoard 9 Bling -Bl ing Кing Price $2 75.00 $48.95 $19.50 $34.95 579,500 .00 $16.00 $29.95 $75.00 $1,200 .00 Aclions 111 ." " 111 ". -- ·-! IИIU 1 . 1 L_________. ___ ___ ____ __ . __I Рис. 11.5 . Редактирование сведений о товаре и отобра жение сообщения из TernpData Чтобы защититься от недопустимых значений данных, свойства класса Product декорируются с помощью атрибутов, как это делалось для класса Order в главе 10 (листинг 11 . 14) . Листинг 11.14. Применение атрибутов проверки достоверности в файле Product. cs using System.Componentмodel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; nam e space SportsStore . Models { puЬlic class Pro duct { puЫic int ProductID { get; set; } [Required(ErrorMessage = "Please enter а product name")] 11 Введите наименование товара puЫic string Name { get ; set; } [Required(ErrorMessage = "Please enter а description")] 11 Введите о п исание puЫic string Descripti on { get; set; } [Required] [Range(0.01, douЬle.MaxValue, ErrorMessage = "Please enter а positive price")] 11 Введите положительное значение для цены
316 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC puЫic decimal Price { get ; set; ) [Required (ErrorMessage = "Pl.ease specify а category") ] //Укажите категорию puЫic string Category { get; set ; } В главе 10 использовался дескрипторный вспомогательный класс для отображения сводки по ошибкам проверки достоверности в верхней части формы. Здесь мы при­ меним похожий подход, но будем отображать сообщения об ошибках рядом с элемен­ тами формы в представлении Edi t (листинг 11.15) . Листинг 11. 15. Добавление элементов для отображения ошибок проверки достоверности в файле Edi t. cshtml @model Prod u ct @{ ViewBag . Title = " Edi t Product "; Layout = "_AdminLayout "; <form asp- action=" Edit " method=" post " > <input type= "h idden " asp - for= " ProductID" /> <div cl ass= " form- group"> <l abel asp-f or=" Name "></label > <div><span asp-validation-for="Name" class="text-danger"></span> </div> <input asp-for=" Name " class=" form - cont rol " /> </div> <div class=" form - group " > <label asp - for= " De scription " ></ l abel> <div><span asp-val.idation-for="Description" cl.ass="text-danger"> </span></div> <textarea a s p - for= " Desc ri ption " c lass= " form - cont r ol " ></textarea> </div> <di v class=" form- group"> <label asp- for= " Category" ></label > <div><span asp-validation-for="Category" class="text-danger"></span> </div> <input asp- for=" Category" c l ass=" form- cont rol " /> </div> <div cl ass=" form-group"> <label asp - for= " Price " ></label> <div><span asp-validation-for="Price" class="text-danger"></span> </div> <input asp-for= "Pri ce" cl ass=" form-cont rol" /> </div> <div class="text-cent er"> <button class =" Ьtn Ьtn - primary " t ype =" submit " >Save</button> <а asp - action= " Index " c l ass =" Ьtn Ьtn - default " >Cancel</a> </div> </form>
Глава 11. SportsStore : администрирование 317 Когда атрибут asp - validation-for применяется к элементу span, он использу­ ет дескрипторный вспомогательный класс, который добавляет сообщение об ошибке проверки достоверности для указанного свойства, если при проверке возникли кюше­ то проблемы. Дескрипторные вспомогательные классы будут вставлять сообщение об ошиб­ ке в элемент span и добавлять элемент в класс inp ut-va lidation- error, кото­ рый позволит легко применять стили CSS к элементам с сообщениями об ошибках (листинг 11.16) . Листинг 11.16. Добавление стиля CSS в файле _AdminLayou t . cshtml <!DOCTYPE html> <html> <head> <meta name= " viewport" content= " width=device - width" /> <l ink rel="stylesheet " asp-href-include="liЬ/boots tra p/dist/css/ * .rnin. css " /> <ti t le>@ViewBag.Title</title> <style> . input-validation-error { border-color: red; background-color: #fee </style> </head> <body class= " panel panel - default " > <d i v clas s="panel - heading " ><h4>@ViewBag . Title</h4></div> <div class= " panel-body " > @if (TempData["message"] != null ) { <div class="ale rt al e r t- success " >@TernpData ["rnessage " ]</div> @RenderBody () </div> </body> </htrnl> Определенный здесь стиль CSS выбирает элементы, которые являются членами класса input - va lidat i on - error, и устанавливает для них границу красного цвета и фон. Совет. Явная установка стилей при использовании библиотеки CSS, подобной Bootstrap, может привести к несоответствиям, когда применяются темы содержимого . В главе 27 будет продемонстрирован альтернативный подход , при котором для применения классов Bootstrap к элементам с сообщениями об ошибках проверки достоверности используется код JavaScript, сохраняя все в согласованном состоянии. Дескрипторные вспомогательные классы для сообщений проверки достоверности можно применять где угодно в представлении , но по соглашению (и это вполне разум­ но) принято размещать их поближе к проблемному элементу, чтобы ввести пользова­ теля в 1typc дела. На рис. 11.6 показано, как выглядят сообщения об ошибках провер­ ки достоверности и отображаемые подсказки, для чего нужно зютустить приложение , отредактировать сведения о товаре и отправить недопустимые данные.
318 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Включение проверки достоверности на стороне клиента В текущий момент проверка достоверности данных применяется, только когда пользователь-администратор отправляет результаты редактирования серверу, но большинство пользователей ожидают немедленного отклика при наличии проблем с введенными данными. CJ Ed~ Product Х f- - С 1ф localhost:60000/Adm1.;/~- ----------·-----·----- ----' tll 1 ··-· -- --· --===== --- --= --=== --== --== ---:::=:.:=--=..:----· -· -· --' 1 Edit Product 1 1 Nam o Please eriter а prodt1ct name 1 / Desc ription 1 Please enter а description , с! Category 1 Please spec~y а category 1 1 1 1 i 1 1 1 1 1 i ---~i 1 1 ~~~::е enter а pos~ive price [ -275.00 ·-- ------ ----- ----- - Canc el i "_____________J !L______ Рис. 11.6. Проверка достоверности данных при редактировании сведений о товаре Именно потому разработчики часто предпочитают выполнять проверку достовер­ ности на стороне клиента, при которой данные проверяются в браузере с исполь­ зованием JavaScript. Приложения MVC могут выполнять проверку достоверности на стороне миента на основе аннотаций данных, применяемых к классу модели пред­ метной области. Прежд е всего, понадобится добавить библиотеки JavaScript, которые предоставят приложению средство проверки достоверности на стороне клиента, что делается в файле bower. j son (листинг 11.17). Чтобы увидеть файл bower . j son, может потре­ боваться выбрать проект SportsStore и щелкнуть на кнопке Show All ltems (Показать все элементы) в верхней части окна Solution Explorer. На заметку! Пакеты проверки достоверности на стороне клиента не установятся корректно, если вы не заменили инструмент gi t среды Visual Studio , как было описано в главе 2.
Глава 11. SportsStore : администрирование 319 Листинг 11.17 . Добавление пакетов JavaScript в файле bower. j son 11 name 11 : 11 asp.net11 , 11 private 11 : true , 11 dependenci e s 11 : { 11 bootstrap 11 : 11 3.3 .6 11 , 11 fontawesome 11 : 11 4.6 .3 11 , "jquery": "2. 2 . 4", 11 jquery-validation11 : 11 1.15.0 11 , "jquery-validation-unoьtrusive": 11 3. 2 . 6 11 Проверка достоверности на стороне клиента построена на основе популярной библиотеки jQuery, которая упрощает работу с АРI - инте рфейсом DOM браузера. Следующий шаг связан с добавлением файлов JavaScript в компоновку, чтобы они за­ гружались , когда используются средства администрирования приложения SportsStore (листинг 11.18). Листинг 11. 18. Добавление библиотек проверки достоверности на стороне клиента в файле _AdminLayou t. cshtml < ! DOCTYPE html > <html> <head> <meta name= 11 viewport 11 content =" wi dth=de v i c e -width 11 /> <link rel= 11 stylesheet 11 asp-href - include= 11 li Ь/bootstrap/d i st/ css/* . min . css 11 /> <title>@Vi e wBag . Ti t le</title> <style> . input- vali dati on-e rror { border-col or: red; background-color : #fee </style> <script asp-src-include= 11 liЬ/jquery/**/jquery.min.js 11 ></script> <script asp-src-include= 11 liЬ/jquery-validation/ * */jquery.validate.min.js 11 > </script> <script asp-src-include="lib/jquery-validation-unoьtrusive/**/*.min.js"> </script> </head> <body class= 11 panel pan e l -default 11 > <div class = 11 panel - headi n g 11 ><h4>@ViewBag . Tit l e</h4></div> <div class= 11 pane l- body 11 > @if (TempData ["message "] != null) { <div cl ass= 11 alert alert- success 11 >@T e mpData[ 11 message "] </d i v> @RenderBody () </div> </body> </html> В доб авл е нной разм е тке для выбор а файлов, которые включаются в элементы s cript , применяется дескрипторный вспомогательный класс. Работа этого процес ­ са объясняется в глав е 25. Он позволяет использовать групповые символы для выбо­ ра файлов JavaScript, а это означает, что приложени е н е ра зрушится в случае , если
320 Часть 1. Введение в инфраструктуру ASP. NET Core MVC имена файлов в пакете Bower изменятся, когда выйдет его новая версия. Однако тре­ буется определенная осторожность, потому что (как будет показано в главе 25) до­ вольно легко выбрать не те файлы, которые ожидались. Включение проверки достоверности на стороне клиента не приводит к каким-либо визуальным изменениям, но ограничения, которые указаны с помощью атрибутов, примененных к классу модели С#, вступают в силу на уровне браузера, пр едотвращая отправку пользователем формы с недопустимыми данными и обеспечивая немедлен­ ный отклик при наличии проблемы. За дополнительными сведениями обращайтесь в главу 27. Создание новых товаров Далее мы реализуем метод действия Crea te () , который указан для кнопки Add Product на главной странице со списком товаров. Он позволит администратору добав­ лять новые элементы в каталог товаров. Добавление возможности создания новых това­ ров требует одного небольшого дополнения в приложении. Это является великолепной демонстрацией мощи и гибкости хорошо структурированного приложения МVС. Для на­ чала добавьте в контроллер Admin метод Create (),как показано в листинге 11.19. Листинг 11.19. Добавление метода действия Create () в файле AdminController. cs using Microsoft.AspNetCore . Mvc; using SportsStore . Models; using System . Linq; namespace SportsStore . Controllers puЫic class AdminController : Controller private IProductRepository repository ; puЬlic AdminController(IProductRepository repo) { repository = repo; puЬlic ViewResult Index() => View(repository.Products); puЫic ViewResult Edit(int productid) => View(repository.Products . FirstOrDefault(p => p.ProductID == productid)) ; [HttpPost] puЬlic IActionResult Edit(Product product) if (ModelState. IsValid) { repository . SaveProduct(product) ; TempDa ta [ " message " ] = $" {product . Name) has been saved "; return RedirectToAction( "Index"); else { 11 Что - то не так со значениями данных return View(product) ; puЬlic ViewResul t Create () => View ( "Edit", new Product ()) ; Метод Create () не визуализирует свое стандартное представление. Взамен он ука­ зывает, что должно использоваться представление Edi t. Применение в методе действия
Глава 11. SportsStore: администрирование 321 представления, которое обычно связано с другим методом действия, вполне допустимо. В рассматриваемом случае мы предоставляем новый объект Product в качестве модели представления , так что представление Edi t заполняется пустыми полями . На заметку! Модульный тест для метода действия Create () не добавляется. Он позволил бы проверить только способность обработки инфраструктурой ASP. NEТ Core MVC резуль­ тата, возвращаем ого методом дей ствия - то, что мы считаем само собой разумеющим­ ся . (Обычн о тесты для функ цио нал ьных с редств инфраструктуры не пишутся, есл и тол ько нет подозрения о нал ич ии дефекта. ) Это единственное изменение, которое потребовалось, поскольку метод действия Edi t ( ) уже настроен на получение объектов Pr odu ct от системы привязки моделей и на их сохранение в базе данных. Чтобы протестировать фующиональность, запус­ тите приложение, перейдите на URL вида /Admin/ In dex, щелкните на кнопке Add Product заполните форму и отправьте ее. Информация, указанная вами в форме, будет использоваться для создания нового товара в базе данных , который затем появится в списке (рис. 11.7). l.. l:J AJi Prod uФ )( t .---·-·----·-·-,· -----------·--··- ~--. 1 ~ .:... ~1~~~:~~0..'1:~°?'1~dn~n~;'' -.-~·--~~-.,_-о,с= ~~ Ci Edit ?tod..ict х С ГQ)~aiho~;.toOOO;Ad;;~i:~Cr~~~e ; - --· =.-;-. ""'" --·==·-"''-'· -~1 Edit Product ! 1 Namo Snorkel Desc ripti on Breat/1.Q underwaler Category Watorsports Price 10.50 t All Products Snorkol tlOS i>eE!J saVl>d Ю Namo 1 Gr(!en Kayalti 2 Lilejacl<e l З Soccer Вoll 4 Corne r Flags s Stnd tum 6 Тltinking Сэр 7 UnslearJy Cliair l 111 Gancol ! 8 Human CllQSS Вo•rd 1 9 Вl lng-Вllng Кing 10 Sno1ko1 Pri ce Acl!OOS $275.00 ~- $48.95 . 1. а111 $19.50 ·- $34.95 -- $79,500 .00 ~- $ 16.00 -- $29.95 аав $7500 -- 51,200.00 IUllll $10.50 ." 1 _._ [1 ~~~~~~~~~--~~~~~~~ ! -~1·~=· __j Рис. 11 .7 . Добавление нового товара в каталог Удалени е товаров Обеспечить поддержку удаления элементов из каталога довольно просто. Для на­ чала добавьте в интерфейс IProductReposi t ory новый метод, как показано в лис­ тинге 11.20 .
322 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC Листинг 11.20. Добавление метода для удаления товаров в файле IProductRepository. cs using System . Collections . Generic ; namespace SportsStore.Models { puЫic interface IProductRepository IEnumeraЫe<Product> Products { get ; void SaveProduct(Product product) ; Product DeleteProduct (int productID); Затем этот метод необходимо реализовать в классе хранилища Entity Framework Core, т.е. EFProductRepository (листинг 11.21). Листинг 11. 21. Реализация поддержки удаления в файле EFProductRepository. cs using System . Collections . Generic; using System . Linq ; namespace SportsStore . Mode l s { puЬlic class EFProductRepository : IProductRepository { private ApplicationDbContext context; puЫ i c EFProductRepository(ApplicationDbContext ctx) { context = ctx; puЬlic IEnumeraЫe<Product> Products => context.Products ; puЫic void SaveProduct(Product product) if (product.ProductID == 0) { context . Products . Add(product) ; else { Product dbEntry = context . Products . F irstOrDefault(p => p . ProductID product . ProductID) ; if (dbEntry != null) { dbEntry . Name = product . Name ; dbEntry . Description = product.Description ; dbEntry . Price = product.Price ; dbEntry . Category = product . Category ; context . SaveChanges() ; puЫic Product DeleteProduct (int productID) Product dЬEntry = context.Products .FirstOrDefault(p => p.ProductID == productID); if (dЬEntry != null) { context.Products.Remove(dЬEntry); context.SaveChanges(); return dЬEntry; Финальный шаг связан с реализацией метода действия Delete () в контроллере Admin. Он должен поддерживать только запросы POST , потому что удаление объектов
Глава 11. SportsStore: администрирование 323 не является идемпотентной операцией. Как будет показано в главе 16, браузеры и кеши вольны выдавать запросы GET без явного согласия пользователя, по этому мы должны проявить осторожность , чтобы избежать внесения изменений как следствия запросов GET . Код нового метода действия приведен в листинге 11 .22 . Листинг 11.22 . Добавление метода действия Delete () в файле Adm.inController.cs using Microsoft . AspNetCore.Mvc ; using SportsStore.Models ; using System .Linq; namespace SportsStore . Contro ll ers puЫic class AdminController : Contro ller private IProductRepository repository ; puЫic AdminController(IProductRepos itory repo) { repository = r epo ; puЫic ViewResult Index() = > View(repository . Product s) ; puЫic ViewResu lt Edit( i nt productid) = > View(repository .Products . FirstOrDefault(p => p.ProductID == productid)) ; [HttpPost] puЫic IActionResult Edit(Product product) if (ModelState . IsValid) { repository . SaveProduct(product) ; TempData[ " message "] = $ "{produ c t.N a me } has been saved "; return RedirectToAction( " Index " ) ; else { 11 Что-то не так со значениями данных ret urn View(product) ; puЬlic IActionResult Create() => View( " Edit" , new Product()) ; [HttpPost] puЫic IActionResult Delete (int productid) { Product deletedProduct = repository.DeleteProduct(productid); if (deletedProduct != null) { TempData["message"] = $ 11 {deletedProduct.Name} was deleted"; return RedirectToAction("Index"); Модульное тестирование: удаление товаров Нам ну ж но протестировать основное поведение метода действия Delete () , которое за­ ключается в том, что при передаче в качестве параметра допустимого идентификатора ProductID метод действия должен вызвать метод DeleteProduct () хранилища и пе­ редать ему корректное значение ProductID удаляемого товара. Вот тест, добавленный в файл AdminControlle rT ests . cs :
324 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC [Fact] puЫic void Ca n_Del ete_Val id_Products( ) { 11 Организация - создание объекта Product Product prod = new Product { ProductID = 2 , Name = " Test" }; //Ор г анизация - создание имитирова н но г о хранилища Mock< IP roduct Repository> mock = n ew Moc k <I ProductRepository>() ; mock . Setup (m => m. Products) .Returns(new Product[ ] { new Product {ProductID = 1 , Name = " Pl"}, prod, new Product {ProductID=3, Name = "РЗ"}, }); //О р ганизация - создание контроллера AdminController target = new AdminCont r oller(mock.Object ); //Действие - . удаление т овара target . Delete(prod.ProductID) ; //Утв ержде н и е - проверка того , что был вызван метод удаления / / в хранилище с корректным объектом Pr oduct mock . Ve rify(m => m. Del e teProduct(prod .ProductID)) ; Чтобы увидеть средство удаления в работе, запустите приложение. перейдите на URL вида /Admi n/Index и щелкните на одной из кнопок Delete (Удалить) на стра­ нице со списком товаров (рис. 11.8). Можно заметить, что с помощью переменной TernpData отображается сообщение об удалении товара из каталога. Ю NAmв 1 Creen!Uyak Price 3 &x::cErВalt 52 75 .00 4 Cornit Fl;ig.s 54595 S St&dium $tQ.50 G Th1nkng Сар $.34 .95 7 UMt1Ыdy C h.tr 1 $79.500 .00 8 Hu nюr1 Ch0s Oo.1rd ! 16.00 ...., 10 Snor!ФI $7500 $ 1.200 .00 -1 Рис. 11.8. Удаление товара из каталога Резюме Acliona m• 1'!:1111 аа Шlll m• ша . 111 lmi a:I ~- В этой главе были введены средства администрирования и показано, как реализо­ вать операции CRUD , которые позволяют администратору создавать, читать , обнов ­ лять и удалять товары из хранилища и помечать заказы как отгруженные. В следую ­ щей главе мы продемонстрируем способ защиты административных функций, чтобы они не были доступны всем пользователям , и развернем приложение SportsStoгe в производственной среде .
ГЛАВА 12 SportsStore: защита и развертывание в предыдущей главе мы добавили в приложение SportsStore поддержку админис­ трирования, и от вашего внимания, вероятно, не ускользнул тот факт, что если развернуть приложение в том виде как есть. то модифицировать каталог товаров смог бы любой пользователь. Для этого ему лишь нужно знать, что средства администри­ рования доступны через URL вида /Adm in / Index и /Order /List . В настоящей главе мы покажем, как предотвратить использование административных функций случай­ ными посетителями, защитив их паролем . Обеспечив возможность защиты, мы объ­ ясним, каким образом подготовить и развернуть приложение SportsStore в производс­ твенной среде. Защита средств администрирования Аутентификация и авторизация предоставляются системой ASP.NET Core Identity, которая аккуратно интегрируется как в платформу ASP.NET Core, так и в приложе­ ния МVС. В последующих разделах мы создадим базовую настройку защиты, которая позволит одному пользователю по имени Adrnin проходить аутентификацию и полу­ чать доступ к административным функциям в приложении. Система ASP.NET Core ldentity предлагает множество других средств для аутентификации пользователей, а также авторизации доступа к функциям и данным приложения. Более подробные сведения вы найдете в главах 28-30, где будет показано, как создавать и управлять пользовательскими учетными записями, каким образом применять роли и политики и 1<ак поддерживать аутентификацию от третьих сторон, таких как Microsoft, Google, Facebook и 1\vltter. Однако цель этой главы - создать лишь столько функциональнос­ ти , сколько достаточно для предотвращения доступа пользователей к чувствитель ­ ным частям приложения SportsStore, что будет содействовать пониманию того, как аутентификация и авторизация вписываются в приложение МVС. Добавление в проект пакета ldentity Первый шаr заключается в добавлении к проекту SportsStore средства ASP.NET Core Identity, которое требует ряда новых пакетов NuGet . В листинге 12.1 показаны добавления в файле project . j son из проекта SportsStore.
326 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Листинг 12.1. Добавление средств а ASP.NET Core ldentity в файле project. j son из проекта SportsStore " dependencies ": { " Microsoft.NETCore . App ": }' "version": "1 .0 .0", " type ": " platform " }' " Microsof t. AspNetCore . Diagnostics ": " 1.0 . 0 ", " Microsoft . AspNetCore . Server . IISintegration ": "1.0 .0", " Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . О ", " Microsof t. Extensions . Logging.Console " : "1. 0 . 0 ", " Microsoft. AspNetCore . Razor . Tools ": { " version": "l. 0 . 0 - preview2 -final ", "type ": " build" }, " Microsoft . AspNetCore . StaticFiles ": " 1. О. О ", " Microsoft . AspNetCore . Mvc ": "1.0. О ", " Microsoft . EntityFrameworkCore . SqlServer ": " 1.0 .0 ", " Microsoft . EntityFrameworkCore . Tools ": " l . O. O- preview2 - final ", " Microsoft . Extensions .C onfiguration . Json ": " 1 . 0 . 0 ", "Mi crosoft. AspNetCore . Session ": "1 .0 . О ", " Microso ft. Extens ions.C aching . Memory ": "1.0 .0", " Microsoft . AspNetCore . Http . Extensions ": " 1 . 0 . 0 ", "Microsoft.AspNetCore.Identity.EntityFrameworkCore": 11 1.0.0 11 После сохранения файла proj ect . j son среда Visual Studio с помощью NuG et за­ грузит и установит пакет Identity. Создание базы данных ldentity Система ASP.NET Core Identity чрезвычайно конфигурируема и расширяема , под­ держивая многочисленные варианты хранения данных о пользователях. Мы собира­ емся ис пользовать наиболее распро страненный вариант, который пр едусматрива ет хранение данных с применением Microsoft SQL Server и доступ к ним с помощью Entity Framework Core. Создание класса контекста Нам необходимо создать файл контекста базы данных, который будет дейс­ твовать в качестве шлюза между базой данных и объектам и моделей Identity, пре­ доставляющими к ней доступ . Добавьте в папку Models файл класса по имени Appidenti tyDbContext. cs с определением, приведенным в листинге 12.2 . Листинг 12.2. Содержимое файла Appiden ti tyDЬCon text. cs из папки Models using Microsoft . AspNetCore . Identity . EntityFrameworkCore ; using Microsoft . EntityFrameworkCore; namespace SportsStore . Mode l s { puЬlic c las s AppidentityDbContext : IdentityDbContext<IdentityUser> { puЫic App i dentityDbContext(DbContextOptions<AppidentityDbContext> options) : base ( options) { }
Глава 12. SportsStore: защита и развертывание 327 Класс Appidenti tyDbContext является производным от класса Identi tyDb Context, который предлагает связанные с Identity средства для Entity Framework Core. В параметре типа используется Identi tyUser, представляющий собой встроен­ ный класс, который применяется для представления пользователей. В главе 28 будет продемонстрировано использование специального класса, который можно расширять с целью добавления дополнительной информации о пользователях приложения. Определение строки подключения На следующем шаге определяется строка подключения, предназначенная для базы данных . В листинге 12.3 показаны добавления, внесенные в файл appsettings . j son про екта SportsStore. которые соответствуют тому же самому формату. что и строка подключения, определенная для базы данных товаров в главе 8 . Листинг 12.3. Определение строки подключения в файле appsettings. j son "Data": { " SportStoreProducts ": { }1 " ConnectionString ": " Server=(local db)\\MSSQLLocalDB ; Database=SportsStore ;T rusted_Connection=True ; MultipleActiveResu ltSe ts=true " "SportStoreidentity": { "ConnectionString": "Server=(localdЬ)\\ MSSQLLoca1DB;DataЬase=Identity; Trusted_Connection=True;MultipleActiveResultSets=true" Вспомните, что строка подключения должна определяться как единственная не­ разрывная строка кода в файле appsettings . j son, а в листинге она выглядит так из-за ограниченной ширины печатной страницы. Добавленная разметка определяет строку подключения по имени Spo r tsStoreidentity , в которой указывается база данных LocalDB под названием Identity. Конфигурирование приложения Подобно другим средств ам ASP.NET Core система Identity конфигурируется в классе Start . В листинге 12.4 приведены добавления для настройки Identity в про­ ект е Spo rts Store с применением ранее определенного класса контекста и строки подключения. Листинг 12.4 . Конфигурирование средства ldeпtity в файле Startup. cs using Mic r osoft . AspNetCore .Builder ; using Microsoft . AspNetCore.Ho s ting ; using Microsoft . AspNetCore .Http ; using Microsoft.Extensions. De pendencyinjection ; using Microsoft . Extensions . Logging; using SportsStore . Models ; using Microsoft.Extensions . Configuration ;
328 Часть 1. Введение в инфраструктуру ASP.NET Core MVC us i ng Mi crosoft.EntityFrameworkCore ; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; namespace SportsStore { puЫic class Startup { IConfigurationRoot Confi guration ; puЫic Startup(IHostingEnvironment env) { Configuration = new ConfigurationBuilder() . SetBasePath(env . ContentRootPath) . AddJsonFile ("appsettings . j son" ) .Build() ; puЬlic void Configu reServices(IServiceCollection services) servi ces.AddDbContext<ApplicationDbConte xt>(options = > options . UseSqlSe r ver( Configuration [" Data : SportStoreProducts : ConnectionStr ing"] ) ); services . AddDbContext<AppidentityDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreidentity:ConnectionString"])); services.Addidentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<AppidentityDЬContext>(); services . AddTransient<IProductRepository , EFProductRepository> () ; services . AddScoped<Cart>(sp => SessionCart.GetCart(sp)) ; services . AddSingleton<IHttpContextAccessor , HttpContextAccessor>(); services . AddTransient<IOrderRepository, EFOrderRepository>() ; services . AddMvc() ; services.AddMemoryCache() ; services . AddSession() ; puЫic void Configure(IApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) app . UseDeveloperExceptionPage() ; app.UseStatusCode Pages() ; app.UseStaticFiles(); app . UseSession() ; app . Useidentity(); app . UseMvc(routes => 11 .. . для краткости маршруты не показаны ... }); SeedData.EnsurePopulated(app) ; IdentitySeedData.EnsurePopulated(app) ; В методе ConfigureServices () конфигурация Entity Framework Core расширена дл я регистрации класса 1юнте кста и с помощью метода Addidenti ty (} устанавли­ вает службы Identity, используя встроенн ые нлассы для представления пользователей и ролей . Внутри метода Configure () вызывается метод Useidenti ty () для уста-
Глава 12. SportsStore : защита и развертывание 329 новки компонентов, которые будут перехватывать запросы и ответы для внедрения политики бе зопасности. Кроме того , добавлен вызов метода Ident i tySeedData . Ens u rePopulated (), ко­ торый будет со здан в следующем разделе для добавления данных о пользователях в базу данных. Определение начальных данных Мы планируем явно создать пользователя Admin, наполняя базу данных началь­ ными данными при запуске приложения. Добавьте в папку Models файл класса по имени IdentitySeedData . cs и определите в нем статический класс, как показано в листинге 12.5 . Листинг 12 .5. Содержимое файла IdentitySeedData. cs из папки Models using Microsoft . AspNetCore.Builder ; using Microsoft . AspNetCore .Identity ; using Microsoft . AspNetCore .I dentity .Ent i tyFrameworkCore ; using Microsoft . Extensions . Dependencyin jection ; namespace Spo rt sStore . Mode l s { puЬlic static class IdentitySeedData pri va te const string adminUse r = " Admin "; private const string admin Passwo rd = "S ecret123$ "; puЬlic static a syn c void EnsurePopu lated(IApp l icationB uil der арр) UserManage r < I dent ity Us er> userManager = app .ApplicationServ ic es . GetRequ iredServ i ce<Us erMa n age r < Ide nti tyUser>>() ; IdentityUser user = await u serManager . FindByidAsync(adminUser); if (user == null) { user = new IdentityUser("Admin" ) ; await userManager . CreateAsync( user, adminPassword) ; В коде применяется класс UserManager<T>, который предоставляется сист е мой ASP.NEТ Core Identity в виде службы для управления пользователями, как описано в главе 28. В базе данных производится поиск учетной записи пользователя Admin, ко­ тор ая в случае ее отсутствия создается (с паролем Secret1 23$). Не изменяйте жестко закодированный пароль в этом примере, поскольку система Identity имеет политику проверки достоверности, которая требует, чтобы пароли содержали цифры и диапа­ зо н символов. Способ изменения настроек , относящихся к проверке достоверности , описан в главе 28. Внимание! Жесткое кодирование деталей учетной записи администратора часто требуется для того, чтобы можно было войти в прило же ние после его развертывания и начать ад­ министрирование. Поступая так, вы должны помнить о необходимости изменения пароля для учетной записи , которую создали . В главе 28 приведены детали того, как изменять пароли , используя ldeпtity.
330 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Соэдание и применение миграции базы данных Все компоненты на месте, так что самое время воспользоваться средством мигра­ ций Entity Framework Core для определения схемы и применения ее к базе данных. Откройте консоль диспетчера пакетов и запустите следующую команду для создания миграции: Add-Migrati on Initial - Co n text Ap pidentityDbCon text Важное отличие от предшествующих команд базы данных касается использования параметра - Co ntext для указания имени класса контекста, ас социированного с ба­ зой данных, с которой нужно работать, т.е . Appidenti tyDbContext. При наличии в приложении нескольких баз данных важно удостовериться, что работа производится с правильной базой. После того как инфраструктура Entity Framework Core сгенерировала начальную миграцию, запустите приведенную ниже команду для создания базы данных и запус­ тите команды миграции: Update-Da t abase - Context Appidenti tyDbCo n text Результатом будет новая база данных LocalDB по имени Identity. которую мож­ но просмотреть с помощью окна SQL Server Object Explorer (Проводник объектов SQL Server) ср еды Visual Studio. Применение базовой политики авторизации Теперь. когда средство ASP.NET Core Identity установлено и сконфигурировано, можно применить политику автори зации к тем частям приложения, которы е необхо­ димо защитить. Мы собираемся использовать наиболее базовую политику авториза­ ции, которая предусматривает разрешение доступа любому пользователю , прошедше ­ му аутентификацию. Хотя она может быть полезной политикой также и в реальном приложении , существуют возможности для создания более детализированных эле­ ментов управления авторизацией (как описано в главах 28-30), но поскольку в при­ ложении SportsStore имеется только один пользователь, вполне достаточно провести разл ичие между анонимными и аутентифицированными запросами. Атрибут Author ize применяется для ограничения доступа к методам действий, и в листинге 12 .6 видно, что этот атрибут используется для защиты до ступа к админи с­ тративным действиям в контроллере Orde r . Листинг 12.6. Ограничение доступа в файле OrderController. cs using Microso ft. AspNetCore . Mvc ; using Sp ortsStore.Mo del s; using System.Linq; using Microsoft.AspNetCore.Authorization; namespace Sport s Store . Co n trollers { puЬlic class OrderControll er : Controll er p r ivate IOrd e r Re posito ry r epository ; pri vate Cart cart; p uЬli c Ord erControl le r (IOrderRepos itory r epo Se rvice , Ca r t cartServi ce ) { r epositor y = repoSer vice; cart = cartService;
Глава 12 . SportsStore : защита и развертывание 331 [Authorize] puЬlic ViewResu lt List() = > View(reposito r y . Orders.Where(o => !o . Shipped)) ; [HttpPost] [Authorize] puЬlic IActionResul t MarkShipped(int orderID) { Order order = repository . Orders .FirstOrDefault(o => o . OrderID orderID) ; if (order ! = null) { order . Ship ped = true ; repository . SaveOrder(order); return RedirectToAction(nameof(List) ) ; puЫic ViewResul t Checkout () => View (new Order ()) ; [HttpPost] puЫic IActionResult Checkout(Order o r der) { if (cart . Lines.Co unt () == 0) { ModelState . AddMode l Error( "", "Sorry, your cart is empty! " ) ; if (Mode l State . IsValid) { order . Lines = car t. Lines . ToArray() ; rep os itory . SaveOrder(order); return RedirectToAction(nameof(Cornp le ted)) ; else { return View(order) ; puЫic ViewResult Completed() { cart.Clear() ; return Vi ew() ; Мы не хотим препятствовать доступу пользователей , не прошедших аутенти­ фикацию, к остальным методам действий в контроллере Order. поэтому атрибут Authorize применен только к методам List () и MarkSh ipped () . Нам нужно защи ­ тить все методы действий, определяемые контроллером Admin, и этог о можно достичь за счет применения атрибута Authorize к самому классу контроллера, что приведет к применению политики авторизации ко всем содержащимся в нем методам действий (листинг 12.7) . Листинг 12.7. Ограничение доступа в файле AdminController. cs using Microsoft . AspNetCore . Mvc ; using SportsStore . Models ; using Systern . Linq ; using Microsoft.AspNetCore.Authorization; namespace SportsSto re.C on trollers {
332 Часть 1. Введение в инфраструктуру ASP.NET Core MVC [Authorize] puЫic class AdminController : Controller { private IProductRepository repos itory; puЫic AdminController(IProductRepository repo) { repository = repo ; puЫic ViewResult Index() => View(repository.Products) ; puЫic ViewResult Edit(int productid) => View(repository . Products . F irstOrDefaul t (р => р . ProductID == productid)); [HttpPost] puЫic IActionResult Edit(Product product) if (ModelState.IsValid) { repository . SaveProduct(product); TempData[ " message "] = $"{product . Name) has been saved "; return RedirectToAction( " Index " ); else { // Что - то не так со значениями данных return View(product) ; puЬlic ViewResul t Create () => View ( "E di t", new Product ()) ; [HttpPost] puЫic IActionResult Delete(int productld) Product deletedProduct = repository . DeleteProduct(productid); if (deletedProduct ! = null) { TempData["message"J = $"{deletedProduct . Name} was deleted "; return RedirectToAction( " Index " ) ; Создание контроллера Account и представлений Когда пользователь. не прошедший аутентификацию, посыл ает запрос, который требует авторизации, он перенаправляется на URL вида /Account/Login, где прило­ жение может пригласить пользователя ввести свои учетные данные. В качестве под­ готовки создайте модель представления для учетных данных пользователя, добавив в папку Models/ViewModels файл класса по имени LoginModel. cs с определением, показанным в листинге 12.8 . Листинг 12.8 . Содержимое файла LoginModel. cs из папки Models/ViewModels using System . ComponentModel.DataAnnotations; namespace SportsStore . Models . ViewModels puЫic class LoginModel { [Required] puЫic string Name { get ; set; }
[Required] [UIHint("password")] Глава 12. SportsStore : защита и развертывание 333 puЫic string Password { get; set; ) puЫic string ReturnUrl { get; set ; } " / "; Свойства Name и Password декорированы атрибутом Required, который исполь­ зует проверку достоверности модели для обеспечения того, что значения были пре­ доставлены. Свойство Password декорировано атрибутом UIHint, поэтому в случае применения атрибута asp-for внутри элемента input представления Razor, пред­ назначенного для входа, дескрипторный вспомогательный класс установит атрибут type в password; таким образом, вводимый пользователем текст не будет виден на экране. Использование атрибута UIHint описано в главе 24. Далее добавьте в папку Controllers файл класса по имени AccountController. cs с определением контроллера, приведенным в листинге 12.9. Этот контроллер будет отвечать на запросы к URL вида /Account/Login. Листинг 12.9 . Содержимое файла AccountController.cs из папки Controllers using System.Threading.Tasks ; using Microsoft .As pNetCore . Authorization; using Microsoft . AspNetCore . Identity ; using Microsoft . AspNetCore .Identity.Ent ityFrameworkCore ; using Microsoft . AspNetCo re.Mvc ; using SportsStore . Models .ViewModels ; namespace SportsStore . Controllers [Authorize] puЫic class Accountcontroller : Controller private UserManager<IdentityUser> userManager; private SigninManager<IdentityUser> signinManager; puЫic AccountController(UserManager<IdentityUser> userMgr , SigninManager<IdentityUser> signinMgr) { userManager = userMgr; signinManager = signinMgr; [AllowAnonymous ] puЫic ViewResult Login(string returnUrl) { return View(new LoginModel { ReturnUrl = returnUrl )); [HttpPost) [AllowAnonymous] [Va lidateAntiForgeryToken ] puЬlic async Task<IActionResult> Login(LoginModel loginModel) { if (ModelState. IsValid) { IdentityUser user = await userManager.FindByNameAsync(loginModel.Name);
334 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC if (user != null) { await signinManager . SignOutAsync() ; if ((await signinManager . PasswordSigninAsync(user , loginModel . Password , false, false)) . Succeeded) return Redirect(loginModel? .ReturnUrl ?? 11 /Admin/Index 11 ); ModelState .AddModelEr r or ( 1111 , 11 Invalid name or password 11 ); return View(loginModel) ; puЫic async Task<RedirectResult> Logout(string returnUrl await signinManager .Si gnOutAsync() ; return Redirect(returnUrl); 11/") { Когда пользователь перенаправляется на URL вида /Account/Login, версия GET метода действия Log in ( ) визуализирует стандартное представление для страницы и создает объект модели представления, включающий URL, на который браузер должен быть перенаправлен, если запрос на аутентификацию завершился успешно. Учетны е данные аутентификации отправляются версии POST метода дейс­ твия Login (), которая применяет службы Us e r Manager<Identi tyUser> и SigninManager<Identi tyUser>, полученные через конструктор класса контрол­ лера, для аутентификации пользователя и его входа в систему. Работа упомянутых классов объясняется в главах 28-30, а пока достаточно знать, что в случае отказа в аутентификации создается ошибка проверки достоверности модели и визуализиру­ ется стандартное представление. Если же аутентификация прошла успешно, тогда пользователь перенаправляется на URL, к которому он хотел получить доступ перед тем, как ему было предложено ввести свои учетные данные. Внимание! В целом использование проверки достоверности данных на стороне клиента яв­ ляется хорошей практикой . Она освобождает от определенной работы сервер и обеспе­ чивает пользователям немедленный отклик о предоставленных ими данных . Тем не менее, не поддавайтесь искушению выполнять на стороне клиента аутентификацию, поскольку это обычно предусматривает передачу клиенту допустимых учетных данных, которые бу­ дут применяться при проверке вводимых имени пользователя и пароля, или , по меньшей мере, наличие доверия сообщению клиента о том , что аутентификация завершилась ус­ пешно . Аутентификация должна всегда выполняться на сервере . Чтобы снабдить метод Login () представлением для визуализации, создайте папку Views/Account и поместите в нее файл представления Razor по имени Login . cshtml с содержимым, показанным в листинге 12.10. Листинг 12.10. Содержимое файла Login.cshtml из папки Views/Account @model LoginModel @{ ViewBag . Title = "Log In "; Layout = 11 _ A dminLayout ";
Глава 12 . SportsStore : защита и развертывание 335 <div class="text - danger " asp - validation-summary= " All " ></div> <form asp-action= " Login" asp-controller="Account" method="post"> <input type= " hidden " asp -for= "R eturnUrl " /> <div class= " form - group " > <label asp - for= " Name " ></label> <div><span asp- validation-for= " Name " class= " text-danger " ></span></div> <input asp - for= " Name " class= " form - control " /> </div> <div class= " form - group " > <label asp - for= " Password" ></label> <div><span asp - validation - for= " Passwo r d " class= " text - danger " ></span> </div> <input asp - for="Password " class= " form - control " /> </div> <button class= " Ьtn Ьtn - p ri mary " type= "s ubmit " >Log In</button> </forrn> Финальный шаг связан с изменением разделяемой компоноюш для админис­ трирования , чтобы добавить кнопку Log Out (Выход). которая позволит текущему пользователю выходить из приложения за счет отправки запроса действию Logou t (листинг 12 .11). Это удобное средство, облегчающее тестирование приложения, без которого пришлось бы очищать сооkiе-наборы браузера , чтобы возвращаться в состо­ яние, 1югда аутентификация еще не прошла. Листинг 12.11. Добавление кнопки Log Out в файле _AdminLayout.cshtml <!DOCTYPE html> <htrnl> <head> <meta name= " viewport " content= " width=de vi ce - width " /> <link rel= "stylesheet " asp- href-include=" liЬ/bootstrap/dist/css/* . min . css " /> <title>@ViewBag . Title</title> <style> . input-validation-error { border- color : red ; background- co l or : #fee </style> <script asp-src-include="liЬ/jquery/**/jquery . min.js " ></script> <script asp -s rc - include= " liЬ/jquery - validation/**/jquery .v alidate . min .j s " > </script> <script asp - src-include= "l iЬ/jquery-validation-unoЬtrusive/**/* . rnin . js " > </script> </head> <body class= " panel panel - default " > <div class= "p anel - heading " > <h4> @ViewBag . Title <а class="Ьtn Ьtn-sm Ьtn-primary pull-right" asp-action="Logout" asp-controller="Account">Log Out</a> </h4> </div> <div class= " panel - body " > @if (TempData[ "rn essage "] ! = null) { <div class= " alert alert - success">@TempData["message"J</div>
336 Часть 1. Введение в инфраструктуру ASP. NЕТ Core MVC @Ren derBo dy () </div> </body> </ht ml> Тестирование политики безопасности Теперь можете протестировать политику безопасности, запустив приложение и запросив URL вида /Adrnin / I ndex . Поскольку в настоящий момент вы еще не про­ шли аутентификацию и пытаетесь обратиться к действию, которое требует автори­ зации , браузер будет перенаправлен на URL вида /Ac count / Lo gi n . Введите Admin и Secret123$ в качестве имени и пароля и отправьте форму. Контроллер Account сравнит предоставленные учетные данные с начальными данными, добавленными в базу данных Identity, и (при условии, что вы ввели правильные сведения) аутентифи­ цирует вас, после чего перенаправит обратно на /Account / Login , куда теперь у вас имеется доступ . Процесс иллюстрируется на рис. 12.1. DLogln --------~----- D AJI Product~ х ~ с ГФ localhos . 0000/Лс г--·.---------------··-- ---· --· -· -, .,. f· -· ---" ---.ic=:-. -::=== -~ -'"-~ '-~~~-=~~~~~:~:;1;~~~="'--=-==---==11_ ~_ !Loln 1g All Products 1111 J Name Ю Namo Ргiсв Act IOns 1 1 1 P::::ro Л · . 1Kayak2 s21s.oo ". - 2 Lilejacket $48.95 ~- -- 1 3 Soccвr 8311 $1 9.50 - - 1 lll!llfl!ll ' 1 ~.. · -. 1 ВМ 4 Come rRags $34 .95 ~- 1______________J 5 S1ad111m $79,500 .00 Ш lll 1 6 Thinking Cap $16.00 _,, . - · ~·~--...... ..,..,....~ ..~..,,,.,...--- Рис. 12. 1. Процесс аутентификации/авторизации для административных функций Развертывание приложени я Все средства и функциональность приложения SportsStore на месте , так что са­ мое время подготовить приложение и развернуть его в производственной среде. Для приложений ASP.NET Core МVС доступно множество вариантов размещения, и в это й главе используется один из них - платформа Microsoft Azure. Она выбрана из-за того, что поступает от Microsoft и предлагает бесплатные учетные записи , т.е. вы можете полностью проработать пример SportsStore, даже если не хотите применять Azure для собственных проектов. На заметку! В данном разделе вам понадобится учетная запись Azure . Если у вас пока ее нет, можете создать бесплатную учетную запись на h ttp : / / a zure .mi c r osoft . с о т.
Глава 12. SportsStore: защита и развертывание 337 Создание баз данных Начальной задачей является создание баз данных, которые приложение SportsStore будет использовать в производственной среде. Такую задачу можно выполнять как часть процесса разработки в Visual Studio, но трудность ситуации в том, что нужно знать строки подключений для баз данных до развертывания, а это относится к про­ цессу , 1tоторый создает базы данных. Внимание! Портал Azure часто меняется по мере того, как в Microsoft добавляют новые средства и пересматривают существующие. Инструкции, приводимые в настоящем раз­ деле, были точными на время его написания, но к моменту выхода книги необходимые шаги могут слегка измениться . Базовый подход должен остаться таким же, но имена по­ лей данных и точный порядок шагов может потребовать определенного экспериментиро­ вания, чтобы добиться правильных результатов. Самый простой подход предусматривает вход на портал h t tp : / /portal . azure . сот с применением своей учетной записи Azure и создание баз данных вручную. Посл е входа выберите категорию ресурсов SQL Databases (Базы данных SQL) и щелкните на кнопке Add (Добавить), чтобы создать новую базу данных. Для первой базы данных укажите имя products. Щелкните на ссылке Configure Required Settings (Конфигурировать обязательные настройки) и затем на ссылке Create а New Server (Со здать новый сервер). Введите имя нового серв ера , 1юторое должно быть уникальным в рамках Azure, и выберите имя пользователя и пароль для администра­ тора. В рассматриваемом примере было указано имя сервера sportsstorecoredЬ , имя пользователя sportsstoreadmin и пароль Secret123$. Вам понадобится ис­ пользовать друго е имя сервера и выбрать более надежный пароль . Выберите место­ положение для базы данных и щелкните на кнопке ОК, чтобы закрыть экран пара­ метров, и далее на кнопке Create (Создать), чтобы создать саму базу данных. Порталу Azure потребуется несколько минут для выполнения процесса создания, после чего база данных появится в категории ресурсов SQL Databases. Создайт е еще один сервер баз данных SQL, указав на этот раз имя iden ti ty. Вместо создания нового сервера баз данных можно применять тот, который был со­ здан ранее. Результатом окажутся дв е базы данных SQL Server, размещаемые Azure, дет али которых приведены в табл. 12.1. У вас будут другие имена серверов баз дан­ ных и навернюш более надежные пароли. Таблица 12.1. Базы данных Azure для приложения SportsStore Имя базы данных products identity Имя сервера sports s torecoredЬ sportsstorecoredЬ Имя пользователя­ администратора sportss t oreadmin spo rtsstoreadmin Открытие доступа в брандмауэре для конфигурирования Пароль Secret 123$ Secr et l 23$ Далее необходимо создать схемы баз данных, и проще всего это сделать, открыв доступ в брандмауэре Azure, чтобы можно было запускать команды Entity Framework Core из машины разработки.
338 Часть 1. Введение в инфрастру к туру ASP.NET Core MVC Выберите одну их двух баз данных в категории ресурсов SQL Databases, щелкните на кнопие Tools (Сервис) и затем щелините на ссылие Ореп iп Visual Studio (Отирыть в Visual Studio). Теперь щелините на ссылие Configure Your Firewall (Конфигуриров ать брандмауэр) , щелкните на кнопке Add Client IP (Добавить IР-адр е с кли ента) и щелкни­ те на кнопке Save (Сохранить). Это позволит вашему текущему !Р-адресу достигать серв е ра баз данных и выполнять команды конфигурирования. (Проинсп ектиро в ать схему базы данных можно , щелкнув на кнопке Open ln Visual Studio, что приведет к открытию Visual Studio и исполь з ованию окна SQL Server Object Explorer дл я и с сл е­ дования базы данных.) Получение строк подключений Вскоре понадобятся строки подключений для новых баз данных . Портал Azure предоставляет эту информацию по щелчку на базе данных в категории р есурсов SQL Databases чер ез ссылку Show Database Connection Strings (Пок азать строки подЮiюч е­ ний для баз данных). Строки подключений предлагаются для разных платформ раз­ рабо тки ; прилож ениям .NET требуются строки ADO .NET. Вот строк а подключ ения , которую портал Azure пр едоставля ет для базы данных ident i t у: Server= tcp : sports s torecoredb . database . windows.net , 1433 ; Data Source=sportsstorecoredb.database . windows . net ; Initial Catalog=product s ; Per si st Se curity I nfo = False ; User I D= {ваше_имя_пользователя}; Раsswоrd= {ваш_пароль}; Рооling=Fаlsе ; Multipl eActiveResultSets=False ; Encrypt=T rue ; TrustServerCertifi cate= False ; Connect i on Timeout=30 ; В зависимости от того, как портал Azure подготовил базу данных , вы будете видеть разные параметры конфигурации . Обратите внимание на отмеченные полужирным заполнител и для им ени пользователя и пароля, которые должны быть изм енены, ког­ да вы применяете строку подключения при конфигурировании приложения . Подготовка приложения Перед те м , каи прилож е ние можно будет развернуть, пр едстоит выполнить опре­ деленные подготовительные шаги, чтобы привести его в готовность к производств ен­ ной среде . В посл едующих разделах будет изменен способ отобр аже ния сообщений об ошибках и настроены строки подключения для производств енных баз данных. Создание контроллера и представления для отображения сообщений об ошибках В настоящий момент приложение сконфшурировано на использование страниц оши­ бок , дружественных к разработчику , которые предоставляют полезную информацию, ког­ да случается пробл ема. Конечные пользователи не должны видеть таиую информацию, поэтому добавьте в папку Con trollers файл класса по имени ErrorController . cs с определением простого контроллера, показанного в листинге 12.12 . Листинг 12.12. Содержимое файла ErrorController. cs из папки Controllers using Microsoft.AspNetCore .Mvc; namespace SportsStore . Controll e rs puЬlic clas s ErrorContro ller : Contro ller puЫic Vi ewResult Error () => Vi ew ();
Глава 12. SportsStore : защита и развертывание 339 В контроллере определено действие Error, которое визуализирует стандартное представление. Чтобы снабдить контроллер представлением, создайте папку Views/ Error, добавьте в нее файл представления Razor по имени Error . cshtml с размет­ кой, приведенной в листинге 12.13. Листинг 12.13. Содержимое файла Error.cshtml из папки Views/Error @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device- width" /> <link rel= " stylesheet " asp-href-include="liЬ/bootstrap/dist/css/* .min. css " /> <title>Error</title> </head> <body> <h2 class= "text-danger">Error. </h2> <hЗ class= "text -danger" >An error occurred while processing your request. </hЗ> </body> </html> Страница ошибки подобного рода является последним ресурсом , поэтому ее луч­ ше сохранить как можно более простой и не полагаться на разделяемые компоновки, компоненты представлений или другие многофункциональные средства. В данном случае мы отключаем разделяемые компоновки и определяем простой НТМL-документ с сообщением о возникновении ошибки, не предоставляя нш<акой информации о том, что произошло. Определение настроек производственных баз данных На следующем шаге создается файл, который будет снабжать приложение стро­ I<ами подключения к его базам данных в производственной среде. Добавьте в проект SportsStore новый файл по имени appsettings . production. j son с применением шаблона элемента ASP.NET Configurati on File (Файл конфигурации ASP.NET) и помести­ те в него содержимое, показанное в листинге 12.14 . Совет. В списке файлов окна Solution Explorer данный файл находится внутри узла appsettings . j son, который понадобится раскрыть, если позже вы захотите отреда к­ тировать этот файл . Листинг 12.14 . Содержимое файла appsettings .production. json "Data": ( " SportStoreProducts": " ConnectionString": "Server=tcp:sportsstorecoredЬ.database.windows.net, 1433;Data Source=sportsstorecoredb . database.windows .net;Initial Catalog= products;Persist Security Info=False;User ID= {ваше_имя_пользователя}; Password={вaш_пap oль}; MultipleActiveResultSets=False;Encrypt=True ; TrustServerCertificate=False;Connection Timeout=ЗO;" },
340 Часть 1. Введение в инфраструктуру ASP. NEТ Соге MVC " SportStoreid e n tity": { " Connecti onStri ng": "Ser ver=tcp: sportsstorecoredb .database . wi n dows . net , 1433 ; Data Sourc e =sport s storecoredb . da t abase . windows . net ; Initial Catalog= identity ; Pers i st Secur ity Info= False ; User ID= {ваwе_имя_ пoльзoвaтeля}; Passwo r d ={вaw_пapoль}; Mult ip leActiveResu l tSets=Fa l se ; Encrypt= T r ue ; TrustServerCertificate= False ; Connect i on Ti meout=ЗO ;" } Файл неудобен для чтения, т.к. строки подключений разбивать нельзя. Содержимое данного файла дублирует раздел строк подключений файла appsettings . j son , но здесь используются строки поднлючений Azure. (Н е забудьте заменить заполнители для имени пользователя и пароля . ) Конфигурирование приложения Теперь можно изменить код класса Star tup, чтобы в случа е н ахождения в прои з ­ водств енной ср еде приложение вело себя по-другому, применяя контроллер Error и строки подключений Azure. Изменения приведены в листинг е 12 .15. Листинг 12.15. Конфигурирование приложения в фа йле Startup. cs us i ng Microsoft . AspNetCore . Bu i lder ; us ing Microsoft . AspNetCore . Hosting ; usi ng Microsoft.AspNetCore .Http; us ing Micros oft.Extensi ons . Dependencyinjection; using Microsoft . Extensions . Loggi n g ; using SportsSto re. Models ; using Mi crosoft . Exten s ions . Co n figur a tion ; using Microsoft . EntityFramewo r kCore ; us i ng Microsoft . AspNetCore . Identity .E ntityFrameworkCore ; namespace SportsStore { puЫic cl ass Startup { ICon figura t ionRoot Conf i guration ; puЫic Star tup (IHosti ngEnvironment env) { Configuration = new Confi gurationBuilder() . S e tBasePath(env . ContentRootPath) .Ad dJsonFile ( " appsetti ngs . j son" ) .AddJsonFile($"appsettings . {env . EnvironmentName}.json", true) . Build() ; puЫ i c vo i d ConfigureServi ces( I ServiceCo l lection services) services . AddDbContext<Appl i ca t ionDbContext>(options => o p tions.UseSqlServer( Confi g u ration[ " Data : Sp ortStor eProdu cts :Connecti on String" ])) ; services . AddDbContext<Ap pidentityDbContext>(options => o p tions . UseSqlServer( Config u ration [" Data : Spo r tS tor eidentity : Co nnectionStri ng" ])) ; services . Addidentity<Ide ntityUse r , IdentityRole>() . A ddEntityFrameworkStor es<Ap p i dentityDbContext>() ;
Глава 12. Spo rtsStore : защита и развертывание 341 services . AddTransient<IProductRepository , EFProd u ctRepository>() ; services .AddScoped<Cart>( sp => Sess i onCart . GetCart(sp)) ; services . AddSingleton<IHttpContextAcces sor , HttpContextAccessor>() ; services.AddTransient<IOrderRepository , EFOrderRepository>() ; services . AddMvc() ; services . AddMemoryCache() ; services.AddSession() ; puЫic void Configure( IAppli cat i on Builder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) ( if (env.IsDeveloprnent()) { app .U seDeveloperExceptionPage() ; app . UseSta t us Code Pages() ; else { app . UseExceptionHandler( 11 /Error 11 ); app . UseStaticFiles() ; app. UseSession( ) ; app . Useide ntity( ); app.UseMvc(routes => routes .MapR oute (narne: 11 Error", ternplate: "Error", defaults: new { controller = 11 Error11 , action = 11 Error11 } ) ; routes . MapRoute( name : null , template: " {cate gory)/Page{page : int) ", defaults : new ( control l er = 11 Product 11 , action ); routes . MapRoute( name : null, template: " Page ( page: int) 11 , defaults : new ( con t roller = " Product ", action page=1) ); routes . MapRoute( name : null , template : " { category} ", defaults: new ( controller page=1 ); routes . MapRoute( name : null , template : 1111 11 Product 11 , action 11 List 11 ) '' List ", 11 List11 , defaults : new ( control ler = 11 Product 11 , action = 11 List ", page=1}); routes . MapRoute(name : null, templ ate : "( controller)/(action)/( id? ) " ) ; }); SeedData . EnsurePopulated(app) ; IdentitySeedData . EnsurePopulated(app} ;
342 Часть 1. Введение в инфраструктуру ASP.NE T Core MVC Интерфейс IHos tingE nv ironmen t используется для предоставления информ ации о среде , в которой функционирует приложение, такой как среда разработки или про­ изводств е нная среда . Мы воспользовались преимуществом этого ср едства для загрузки разных конфи­ гурационных файлов с подходящими строками подключений, ориентированными на ср еду разработки и производств е нную среду . а также изм енения набор а ко м пон е нтов. которы е применяются для обработки запросов. Таким обр азом, средства , сп ецифич­ ны е для разработки, вроде Brows er Link не включаются, когда прилож ение разверты­ вает ся. До ступно множество параметров для настройки конфигурации прилож ения в различных средах. которые будут рассматриваться в главе 14. Обновление конфигурации проекта Осталось внести несколько финал ьных корректирово к в файл proj ect. j son про­ екта SportsSto r e , чтобы обеспечить развертыв ание правильных част ей прилож е ­ ния (листинг 12 . 16) . Листинг 12. 16. Обновление файла project. j son проекта Spo rtsStore " dependencies " : { " Microsoft . NETCore . App " : "version": "1.0.О", " type ": " platform " }' }' " Microsoft.AspNetCore.Diagn ostics ": "1 . 0 . 0 ", " Microsoft.AspNetCore . Serve r . IISintegration " : " 1 . 0 . 0 " , "Microsoft.AspNetCore.Server. Kestr el": "1 .0 .0 " , "Microsoft . Extensions . Loggi ng .Console ": " 1 . 0.0 ", " Microsoft . AspNetCore . Razor . Too l s ": { }' " version": " l .0 .0-preview2-final", "type" : "build" "Microsoft.AspNetCore.StaticFiles": "1 .0 .0 ", "Microsoft .AspNetCore.Mvc": "1 .О . О", " Microsoft . EntityFrameworkCore . Sql Ser ver ": " 1 . 0 . О ", " Microsoft . EntityFrame workCore .Too l s ": " l . 0 . 0 - previ ew2 - final ", "Microsoft.Extensions.Configuration.Json": "1 .0 .0", "Microsoft.AspNetCore.Sessi on": "1 .0 .0" , "Mi crosoft .Extensions . Caching .Memory": " 1 . 0 . 0 ", " Microsoft . AspNetCore .H ttp . Extensions ": " 1 . 0 . 0 ", " Microsoft . AspNe t Core . Ident ity . Ent i ty Frame workCore " : 01 1.0 .0" "tools": { }' " Microsoft . AspNetCore . Razor . Tools ": " 1 . О . O- preview2-final ", " Microsoft . As p NetCore . Server . IISintegration . Tools " : " l .0 .0-preview2-final", " Microsoft . Enti tyFrameworkCore . Tool s ": { "version" : "l .0 .0 -previ ew2-final", " impo r ts ": [ " portaЫe - net4 5+win8+dnxcore50 " , " portaЬle -n et4 5+w i n8 " ]
"f rarneworks ": { " netcoreappl . 0 ": Глава 12 . SportsStore: защита и развертывание 343 " irnports ": [ "dotnetS . 6 ", "portaЫe - net45+win8 " ] }1 "buil dOptions ": }1 " ernitEntryPoint ": true , " preserveCompilationCont ext": true " runtirneOptions ": { " configProperties ": " Systern . GC . Server ": true }1 "puЫishOptions": 11 include 11 : [ "wwwroot", "Views", "appsettings. json", 11 appsettings. production. j son 11 , "web. config 11 ] }1 " scripts ": { "postpuЫ ish ": [ " dotne t puЫish-iis --p uЫ ish-fol der % puЬlish : OutputPath % -- frarnework % puЫish:FullTargetFrarnework % " Добавления к разделу puЫishOptions включают в процесс развертывания ос­ новные части проекта, в том числе представления Razor и конфигурационный файл, который содержит строки подключений для производственных баз данных . Применение миграций баз данных Чтобы настроить базы данных с помощью схем, требующихся приложению, от- кройте консоль диспетчера пакетов и запустите следующие команды: Update - Database - Context ApplicationDbContext - Environment Production Update - Database - Context AppidentityDbContext - En vironme n t Production В параметре - Envirorunent у~<азывается среда размещения, которая используется для получения строк подключения к базам данных. Если команды не выполнились, тогда удостоверьтесь, что сконфигурировали брандмауэр Azure на разрешение досту­ па вашей машины разработки , как было описано ранее в главе. Процесс развертывания приложения Чтобы ра з вернуть прило жени е, щелкните правой кнопкой мыши на элементе про­ екта SportsStore в окне Solution Explorer (проекта, но не решения) и выберите в кон­ текстном меню пункт PuЫish (Опубликовать). Среда VisuaJ Studio предложит выбрать метод опубликования (рис. 12.2). Выберите вариант Microsoft Azure Арр Service (Служба приложения Microsoft Azure). Бам будет предложено ввести данные учетной записи Azure. Щелкните на кнопке New (Новая). как показано на рис. 12.3 .
344 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Sefect а puЫish target l41$ Microsoft Azure Арр Setv\c~ f @ lmpoit 1D fi/e System Find oth•r ho sting options at ou r~~ Рис . 12.2. Выбор метода опубликования х ~ Арр Service l:ji:J Host yo ur vJe b and moЬi le app lica tions. REST APls. and mo re in Azure Subscriptio n View ' R~~u~~e G10~.-. ----- ------·- ·-· -- Se~rch New" . ок [~ Рис. 12.З. Создание новой службы приложения Azure В следующем диалоговом окне предлагается сконфигурировать настройки веб-при­ ложения Azure (рис. 12.4). Выберите имя для приложения , которое должно быть уни­ кальным в Azure, поскольку по умолчанию все приложения совместно используют общее доменно е имя . В качестве имени выбрано s p o r ts s toreco r e , т.е. развернутое прило­ жение будет доступно по адресу h ttp : / / spo r tsstorecore . az u rewebsi tes . сот. Далее введите или выберите группу ресурсов из раскрывающегося списка . Группа ресурсов применяется для категоризации облачных активов , созданных для прило­ жения, и полезна при управлении крупными развертываниями разных приложений. В этом примере создан а группа р е сурсов под названием Sp o rt s S tore .
Глава 12. SportsStore: защита и развертывание 345 f:4J Create Арр Service UU Host your 1veb and mobile applicat.ions. REST APls. and more in Azure , •• HostiП<J @ Servic.es Yleb Арр Nome ;s~Ort~rtor~.'~~ . ___ ."- -· Subscription L1Wi ndoм Azur~ мSoN·- Visu; IStudio Ui ~;at~ ~ -- ------- --------- -----·---~ R~ource Group ~~ort~Stor; . _ Арр Servicc Р lм ---- ----~J Clicking the Cre• te Ьutton win cr e.a te the ro1 &owing Azur e resour ces Арр Servicc - spo rt sstoreco rc New ... 1(1 lf you havc removed yo ur spcndi ng limit oryou arc u~ing Р4у гs Vou Go, therc may Ьс monct ~нy impact if you provision "dditional r~ou rcc.s. t~~111 ~-!cr~ Ьoport" . Cre3te Canccl Рис. 12.4 . Конфигурирование службы приложения х Потребуется также создать план обслуживания. Щелкните на кнопке New (Новый) и введите имя. выберите регион и укажите размер, который будет использоваться для размещения приложения (рис. 12.5). В рассматриваемом примере указан план по имени SportsStoreCorePlan, находящийся в регионе восточных США (East US) и применяющий вариант размера Free (Свободный). Щелкните на кнопке ОК , чтобы сохранить план обслуживания, и затем на кнопке Create (Создать) для продолжения процесса развертывания. Когда среда Visual Studio завершит настройку развертывания, вы увидите экран PuЫish (Опубликование), пред­ ставленный на рис. 12.6 . х ~r"] Configure Арр Service Plan [ji:J An Арр Se rv ice plan is the container for your арр. The Арр .. . Арр Service Plan ~ё_~~s!~Core!~~-~:--~=---=~-=~-:_-=-_:_~~.::=-- =~~=- ·=~~== =] Location r~~rt.~~- .=~~--=--~-_=~==~~==-~~==-=-~=- ~~=--===--- -":J Size 1 . -·--- --- --- ·-· ---·--· __ "____ ___ __ --] Free " - ·-----~·----·----~·-·--- _____ _____, ________ - - ОК 1 \ Canc~I Рис. 12.5. Выбор плана обслуживания
346 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC ф PuЬlish Profile sportsstorecorc - Web D<>ploy Conncctto~ PuЫish method : Гwd;~oy --====--==~-------·-3_-~ Setti ng s Prtview ..-------------· 1sportssto r ec~m.az u rewebsit~. n e:t;443 -- -=:J Servll!r: Site name: Use rn ame: Password: 1··········~ ················~ ~-;;;;.~~~~~~ 0 Save password Dt•ti natio n URl: \ htt p://sportsstorecoro.azurewebsi tes.ne_t _____ _ r_._J Validate Conn ection 1 < Prev j[§:J1 PuЫish 11 Close [____ Рис. 12.6. Подготовка к опубликованию приложения Последний шаг заключается в щелчке на кнопке PuЫish (Опубликовать). Среда Visual Studio начнет процесс опубликования и развернет приложение в облаке Azure. Процесс может занять некоторое время, потому что начальное развертывание проек­ та требует передачи большого количества файлов: последующие обновления проходят быстрее, т.н:. загружаются только новые и измененные файлы. Когда процесс опубликования завершается, платформа Azure запустит приложе­ ние, а среда Visual Studio откроет окно браузера с URL размещения (рис. 12.7) . Резюме В этой и предшествующих главах демонстрировалось использование инфраструк­ туры ASP.NET Core MVC для создания реалистичного приложения электронной ком­ мерции. В приведенном расширенном примере были проиллюстрированы многие основные средства MVC : контроллеры, методы действий, маршрутизация, представ­ ления, метаданные, проверка достоверности, компоновки, аутентификация и т.д . Вы также видели, как пользоваться рядом ключевых технологий, имеющих отношение к МVС, в том числе Eпtity Framework Core, внедрение зависимостей и модульное тести­ рование. Результатом стало приложение с ясной и ориентированной на компоненты архитектурой, обеспечивающей разделение обязанностей, и кодовой базой , которую будет легко расширять и сопровождать. На этом разработка приложения SportsStore завершена. В следующей главе вы научитесь применять Visual Studio Code для созда­ ния приложений ASP.NET Core MVC.
1 1 ! Глава 12. SportsStore : защита и развертывание 347 CJ Spon<Store Х С \ф sp~~s~orec~r~:z~~re\~:bsites.11et ----=~==-~--=====--~ SPOПTS STOHE а Ho me Chess Soccer Wate rsports Kayak А boat for one person lifejacket Protective and fashionaЫe Soccer Ball FIFA-approved size and weight IЩfj•l•I 11\lli.P IJ'!M•• liih Corner Flags litИJ Give your playing field а profess ional touch 811 • 2.3 1 1 1 1 1 1 L-------- ·- -- --- --- ·- ·· -- -- --- --· J Рис . 12. 7 . Развернутое прило жение
ГЛАВА 13 Работа с Visual Studio Code в настоящей главе будет показано, как создать приложение ASP.NET Саге MVC с применением Visual Studio Code - межплатформенного редактора с открытым кодом производства Microsoft. Несмотря на название, продукт Visual Studio Code не имеет отношения: к Visual Studio и основан на инфраструктуре Electгon, используе ­ мой редактором Atom, который популярен среди разработчиков, применяющих дру­ гие инфраструктуры для построения веб-приложений, такие как Angular. Продукт Visual Studio Code поддерживает операционные системы Windows , OS X/macOS и наиболее популярные дистрибутивы Llnux. Продукт Visual Studio Code находится на начальной стадии своего развития, поэтому не все средства работают должным образом ; однако в Microsoft предлагают ежемесячные обновления: и про­ движение происходит довольно быстро. Некоторые текущие ограничения, такие как отсутствие поддержки отладки для приложений ASP.NET Саге, могут быть устранены ко времени выхода этой книги, но вьmолнение всех примеров в данной книге по-пре­ жнему требует Visual Studio и Windows. Процесс разработки в Visual Studio Code менее автоматизирован, чем в полной версии Visual Studio, но он вполне осуществим, предлагая: пристойную отправную точку для построения приложений ASP.NET Core MVC в средах других операционных систем или легковесную альтернативу Visual Studio 2015 в Windows. На заметку! В Microsoft заявили, что инструментарий, используемый для создания прило­ жений ASP. NEТ Core MVC, в будущем изменится. Проверяйте веб-сайт издательства на предмет обновлений, которые появятся после выпуска новых инструментов . Настройка среды разработки Настройка Visual Studio Code требует выполнения определенной работы, посколь­ ку некоторая функциональность, включенная в Visual Studio, обрабатывается здесь внешними инструментами. Одни инструменты идентичны тем, которые среда Visual Studio применяет "за кулисами", но другие являются: новыми в мире разработки для .N E T и могут быть незнакомыми. Хорошая новость в том. что эти инструменты ши­ роко используются разработчиками. которые ориентируются на другие инфраструк­ туры для построения: веб-приложений, причем качество и функциональные средства находятся на высоком уровне. В последующих разделах мы исследуем процесс уста­ новки Visual Studio Code наряду с важными инструментами и дополнениями, требу­ ющимися: для: разработки приложений MVC.
Глава 13 . Работа с Visual Studio Code 349 Установка Node.js В мире разработки клиентской стороны Node.js (или просто Node) представляет собой исполняющую среду, на которую полагаются многие популярные инструменты разработки. Node была создана в 2009 году как простая и эффективная исполняющая среда для серверных приложений, написанных на языке JavaScript. Она основана на механизме JavaScript, применяемом в браузере Chrome, и предлагает АРI-интерфейс для выполнения кода JavaScript за пределами среды браузера. Исполняющая среда Node.js достигла определенных успехов в качестве сервера приложений, но в этой главе она интересна тем, что предоставляет основу для нового поколения межплатформенных инструментов построения и диспетчеров пакетов. Ряд интелле1пуальных проектных решений, внедренных командой разработчиков Node, и межплатформенная поддержка, обеспечиваемая исполняющей средой JavaScript бра­ узера Chrome, создали благоприятную возможность, которой воспользовались полные энтузиазма проектировщики инструментов, особенно те из них, кто желал поддержи­ вать разработку веб-приложений. На заметку! Доступны две версии Node.js. Версия LTS (Loпg Term Support - долгосрочная поддер жка ) предоставляет стабильный фундамент для развертывания в производствен­ ных средах, где изменения должны быть сведены к минимуму. Обновления версии LTS выпускаются каждые 6 месяцев и сопровождаются в течение 18 месяцев. Версия Curreпt (текущая) является более часто изменяющимся выпуском, в котором преимущество от­ дается новым средствам взамен стабильности . В настоящей главе мы будем применять выпуск Curreпt. Установка Node.js в Windows Загрузите и запустите установщик Node.js для Windows из веб-сайта http: / / nodej s. org . При установке Node.js удостоверьтесь, что выбран вариант Add to РАТН (Добавить в переменную РАТ Н). На рис. 13.1 показан установщик для Windows, ко­ торый предлагает модифицировать переменную среды РАТН в качестве варианта установки. Установка Node.js в 05 X/mac OS Установщик для OS X/macOS может быть загружен из веб-сайта http : / / nodej s. org. Запустите установщю< и примите стандартные настройки . Когда уста­ новка завер шится, удостоверьтесь в наличии пути /usr / local/Ьin в переменной $РАТН. Установка Node.js в Linux Самый простой способ установки Node.js в Linux предполагает использование дис­ петчера пакетов; команда разработчиков Node предоставила инструкции для основ ­ ных дистрибутивов по адресу ht tp: / /nodej s . org / en/download/package - manager . В среде Ubuntu для загрузки и установки Node.js прим еняются следующие команды: curl - sL https:// deb .nodesource.com/setup_б.x 1 sudo -Е bash -sudo apt- get install -у nodejs
350 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 1 Custoni 5etup Select lhe way you want features to Ье instaИed. nd~ г "",::;::-.~.... ".._ ""'' "'"~"""...., @j ---- 1 InstaП the core Node .js runtime (node .exe). Тhis feature req uires 16МВ on your hard drive. Ithas 2of 2 suЬfeaЬJres selected. Тhе subleatures re QUir e 2ОКВ on your hard drive. Рис. 13.1. Добавление в переменную среды РАТН Проверка установки Node После завершения установки откройте новое окно командной строки и запустите показанную ниже команду: node -v Если установка прошла успешно, а путь к Node бьm добавлен в переменную среды РАТН, тогда вы увидите номер версии. На момент написания главы текущей версией Node являлась 6.3 .0 . Если во время проработки примеров, рассмотренных в главе, вы получаете неожиданные результаты, то попробуйте воспользоваться указанной кон­ кретной версией. Установка Git Продукт Visual Studio Code вЮiючает интегрированную поддержку Git, но требует­ ся отдельная установка для поддержки инструмента Bower, который прим еняется при управлении пакетами юшентской стороны. Установка Git в Windows или OS X/macOS Загрузите и запустите установщик из веб-сайта httрs ://git - sст. сот/ down loads. Установка Git в Linux В большинстве дистрибутивов Linux инструмент Git уже установлен. Если вы хо ­ тите установить его в любом случае, тогда ознакомьтесь с инструкциями по установке для вашего дистрибутива по адресу https: //git -scт. com/download/linux.
Глава 13. Работа с Visual Studio Code 351 В среде Ubuntu используется следующая команда: sudo apt -get install git Проверка установки Git После завершения установки запустите приведенную ниже команду в новом окне командной строки/терминала, чтобы проверить , установлен и доступен ли инстру­ мент Git: git --version Команда выведет версию установленного пакета Git . На момент написания главы по следней версией Git для Windows была 2.9 .0 , а для OS X/macOs/Linux - 2 .8 .1 . Установка Yeoman, Bower и Gulp Node .js поступает с диспетчером пакетов Node (Node Package Manager - NPM), ко­ торый применяется для загрузки и установки пакетов разрабоп<и. написанных на JavaScript. Пакеты, полезные для разработки прилож ений ASP.NET Core MVC, описа­ ны в табл. 13.1. Таблица 13.1. Пакеты NPM, полезные для разработки приложений ASP.NET Core Имя уо bower generator- aspnet Описание ПакетУеоmаn (известный как уо ) - это инструмент, который об­ легчает начало новых разрабатываемы х проектов , устанавливая исходное содержимое, как демонстрируется в разделе "Создание проекта ASP.NET Core " далее в главе Это тот же самый инструмент Bower, описанный в главе 6, кото­ рый используется для управления пакетами клиентской стороны Этот пакет снабжаетУеоmаn информацией, необходимой для со­ здания новых проектов ASP.NET Core MVC, как объясняется в раз­ деле "Создание проекта ASP.NET Core" далее в главе В среде Windows указанные пак ет ы устанавливаются с помощью такой команды: npm install - g yo@l . 8 . 4 bower@l.7 .9 generator - aspnet@0 . 2 .1 Ко времени выхода книги могут появиться бол ее новые версии этих пакетов, но при с о здании при ме ра применялись ве рсии, упомянутые в коман дах установки . В средах Linux и OS X/macOS установка производится посредством команды sudo: sudo npm install -g yo@l.8 .4 bower@l .7 . 9 generator- aspnet@0 .2 .1 Установка .NET Core При разработке приложений ASP.NET Core MVC требуется исполняющая среда . NET Core. Для каждой поддерживаемой платформы предусмотрен собств енн ый про ­ цесс устан овки , описание которого доступно по адресу www . rnicrosoft . corn/net/ core. В Microsoft предлаг ают установщики для Windows и OS X/macOS, а также пре­ доставляют инструкции для установки в Linux с использованием архивов tar.
352 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Установка .NET Core в Windows Чтобы установить .NET Core в Windows, просто загрузите и запустите установщик .NE T Core SDK (который отделен от установщика .NET Core для Visual Studio, необхо­ димого в главе 2). Установка .NET Core в OS X/macOS Перед запуском установщика .NET Core в среде macOS должна быть установлена последняя версия пакета OpenSSL; в Microsoft рекомендуют применять для этого дис­ петчер пакетов Homebrew. Откройте новое окно терминала и выполните следующую команду, чтобы установить Homebrew: /usr/Ьin/ruby - е " $ (curl -fs SL https : //raw.githubusercontent.com/Homebrew/install/master/install)" Для обновления пакета OpenSSL запустите показанные ниже команды: brew update brew install openssl brew link --force openssl Загрузите установщик .NЕТ Core, находящийся по адресу h t tps : / / go. rnicrosoft . corn/fwlink/?LinkID=809124, и запустите его, чтобы добавить .NET Core в свою систему. Установка .NET Core в Linux По адресу www.microsoft . com/net/core пр едоставлены инструкции по уста­ новке .NET Core в большинстве популярных дистрибутивов Linux. В настоящей главе используется Ubuntu, и процесс требует первоначальной настройки новой подачи для apt-get с применением следующих команд: sudo sh -с ' echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet/ trusty main" > /etc/apt/sources . l is t . d/dotnetdev .l ist' sudo apt-key adv --keyserver apt-mo.trafficmanager.net -- rec v-keys 417А0893 sudo apt - get update Далее производится установка .NЕТ Core: sudo apt-get install dotnet-dev-1.0.0-preview2-003121 Проверка установки .NET Core Независимо от имеющейся платформы проверка того, что инфраструктура .NET Core установлена и готова к использованию, осуществляется одинаково. Откройте но­ вое окно командной строни/терминала и выполните таную команду: dotnet --version Команда dotnet запускает исполняющую среду .NET, после чего отобразится но­ мер версии установленного пакета .NET. На момент написания главы текущим выпус­ ком был 1. О. 0-preview2 - 003121, но ко времени выхода книги он наверняка будет заменен более новым выпуском.
Глава 13 . Работа с Visual Studio Code 353 Установка Visual Studio Code Самым важным шагом является загрузка и установка редактора Visual Studio Code , который доступен на веб-сайт е h ttp : //code . vis u alstudio . сот . Установочные па­ кеты предусмотрены для Windows, OS X/macOS и популярных дистрибутивов Linux. Загрузит е и установите пакеты для предпочитаемой платформы . На заметку! Компания Microsoft предлагает новые выпуски Visual Studio Code ежемесячно, а это значит, что устанавливаемая вами версия будет отличаться о версии 1.3 , которая при­ меняется в главе. Хотя основы должны остаться теми же самыми, выполнение некоторых примеров может потребовать определенной доли экспериментирования. Установка Visual Studio Code в Windows Чтобы установить Visual Studio Code для Windows, просто запустите установщик. После завершен ия процесса Visual Studio Code запустится, и вы увидите окно редак­ тора, представленное на рис. 13.2. Установка Visual Studio Code в 05 X/macOS Продукт Vi s ual Studio Code предоставляется в виде архива zip для Мае , который доступ ен для загрузки по адресу https : / / g o .m icrosoft. com/fwl i nk/ ? Lin kI D= 620882 . Раскройте архив и дважды щелкните на содержащемся в нем файле Visual St u dio Code. а р р , чтобы запустить Visual Studio Code и получить окно ре­ дактора. пока з анное на рис. 13.2 . Установка Visual Studio Code в Linux Компания Microsoft предлага ет файл . deb для DeЫan и Ubuntu и файл . rpm для Red Hat, Fedora и CeпtOS . Загрузите и установите файл для подходящего дистрибути­ ва Linux. Поскольку в главе используется Ubuntu, понадобится загрузить файл . deb и установить его с помощью следующей команды: sudodpkg -i code 1.3 .0-1467909982 arnd64.deb После завершения установки выполните приведенную ниже команду , чтобы запус­ тить Visu a l Studio Code и получить окно редактора, п01шзанное на рис. 13.2: /usr /share/ code/ code Проверка установки Visual Studio Code Тестирование успешности установки Visual Studio Code сводится к проверке воз­ можности запуска приложения и открытию окна редактора (см. рис. 13.2). (Цветовая схем а была изм ен ена , т.к . стандартные темные цвета не очень хорошо подходят для получ ения экранных снимков .) Устано в ка расширения С# для Visual Studio Code Редактор Visual Studio Code поддерживает функциональность , специфичную для языка. через расширения, хотя это не те же самые расширения, которые поддержи­ ваются ср едой Visual Studio 2015. Самое важное расширени е , насающееся разработ­ ки приложений ASP.NET Core MVC, добавляет поддержку для С# , отсутствие которой может выглядеть как странно е упущение в базовой установке.
354 Часть 1. Введение в инфраструктуру ASP.NET Core MVC ----·------------- Vntltled·1 - v1 1u;:,1Studlo Code . ·• Unt•tltd·1.'V.sщ1S1~ю.-.Cod~ . .~, . ,.. ~~~.,~·· ,· '- • . ~.:-'\~D'Х' Рис. 13.2. Выполнение Visual Studio Code в средах Windows, OS X/macOS и Ubuntu Linux Тем не менее, такое положение дел отражает тот факт, что в Microsoft позициони ­ ровали Visual Studio Code как универсальный межплатформенный редактор, который поддерживает широчайший спектр я зыков и инфраструктур. Чтобы установить расширение С#, выберите пункт Command Palette (Палитра ко­ манд) в меню View (Вид) редактора Visual Studio Code. Палитра команд предоставляет доступ ко всем командам, которые можно выполнять с помощью Visual Studio Code . Введите ext и нажмите на клавишу <Return>. в результате чего Visual Studio Code от­ кроет окно Extensions (Расширения). Введите csharp и отыщите в списке расширение С# for Visual Studio Code (С# для Visual Studio Code), как показано на рис. 13.3 . Щелкните на кнопке lnstall (Установить), после чего Visual Studio Code загрузит и установит расширение. Щелкните на кнопке ЕnаЫе (Включить), чтобы активизиро­ вать расширение (рис. 13.4). rn ...1 С# Litense Microsoft 1 'i'331463 j ***У 36 С# for Visual St udio Code (powered Ьу OmniSharp) Mo.stor Dirv Рис. 1З.З. Нахождение расширения С#
Глава 13 . Работа с Visual Studio Code 355 С# license Microsoft 1 4-> 331463 1 * * *1. 36 О for Visual Studio Code (pov;ered Ьу OmniSJ1arp). [!zma ~Nnstan' Рис. 13.4 . Включение расширения С# Создание проекта ASP.NET Core Редактор Visual Studio Code не имеет интегрированной поддержки для создания проектов ASP.NET Core и в настройке начальной структуры папок и файлов пола­ гается на пакет Yeoman, который применяет шаблоны, предоставляемые пак етом generator - aspnet. Откройте новое окно командной строки/терминала, перейдите в каталог, где нужно создать про екты ASP.NET Core, и выполните следуЮIЦУЮ команду: уо aspnet Эта команда запускает пакет Yeoman и сообщает ему о необходимости создания нового проекта ASP.NET Core . Весь процесс настройки проекта производится через командную строку, переходя по параметрам с использованием клавиш со стрелками и делая выбор с применением клавиши <Return>. В табл. 13.2 описан набор шаблонов проектов, доступных для разработки приложений ASP.NET Core. (Есть еще несколь­ ко других шаблонов, которые здесь не перечислены, но они не предназначены для ASP.NEТ Core.) Таблица 13.2 . Шаблоны проектов ASP.NET Core из Yeoman для разработки приложений ASP.NET Core Имя Empty Web Application (Пустое веб-приложение) Web Application (Веб-прило жение) Web Application Basic (Базовое веб-приложение) Web API Application (Веб-приложение API) Описание Этот шаблон создает проект ASP. NEТ Core с минималь­ ным начальным содержимым; он похож на шаблон Empty (Пустой) в Visual Studio 2015 Этот шаблон создает проект ASP.NEТ Core с начальным содержимым, которое включает контроллеры, пред­ ставления и аутентификацию. Он похож на шаблон Web Application (Веб-приложение) в Visual Studio 2015 с вклю­ ченной аутентификацией Этот шаблон создает проект ASP.NET Core с начальным содер жим ым, которое включает контроллеры и представ­ ления, но не аутентификацию Этот шаблон создает проект ASP.NET Core с контроллером API (который будет описан в главе 20) . Он эквивалентен шаблонуWеЬ API в Visual Studio 2015
356 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Выберите шаблон Empty Web Applic a t i on и нажмите <Return>. В ответ на за­ прос имени для проекта введите Part y inv i te s. Вот вывод, который вы будете видеть во время создания пр о екта: ? What t ype of application do you want to c r eate? Empt y Web Application ? Какой тип приложения вы хотите создать ? ? What ' s the name of your ASP.NET applicati on? (EmptyWebAppli cati on) Partyinvi t es ? Како в о имя вашего приложения A SP . NE T ? ( Empt yWe ЬApplication ) Нажмите < Re tu rn>; пакет Yeoman создаст папку Partyinvi tes и наполнит е е ми­ нимальным набором файлов, которые требуются для проекта ASP. NEТ Core. Подгото в ка пр ое кта с помощ ью Visual Stud io Cod e Чтобы открыть проект в Visual Stud io Code, выберите пункт Open Fol d e r ( Открыть папку) в меню File (Файл), перейдите в папку Partyinvit e s и щелкните на кнопке S e le ct Folde r (Выбрать папку) . Редактор Visual Studio Code откроет проект, и по про­ шествии нескольких секунд вы увидите сообщение, предлагающее добавить элементы в проект (рис . 13.5). w~~т~ 'iii~ Required assets to build and debug are miss1n9 from your pro;ect. Add them? ~~~~~ Рис. 13.5. Приглашение добавить активы в проект Щелкните на кнопке Yes (Да). Редактор Visual Studio Code создаст папку . vscode и добавит в нее файлы, которые конфигурируют процесс построения . По умолчанию Visual Studio Code использует компоновку из трех разделов. Боковая панель, выделен­ ная на рис . 13 .6 . предоставляет доступ к главным областям функцион ал ьности . V1~v1 Go to H~lp -4 • .vscode ' ~ Properties 1 • \WMГ OOI \ .gitlgnore Dockerfile } Program .cs projectJso11 ..; project.lock.Json ~ README.md Star1up.cs ~' \veb.config Рис. 13 .6 . Боковая панель Visual Studio Cod e Самая верхняя кнопка позволяет от­ крыть панель проводника, которая отоб­ разит содержимое ранее открытой папки. Остальные кнопки обеспечивают доступ к средству поиска, к интегрированному управлению исходным кодом, к отладчи ­ ку и к набору установленных расшире­ ний . Щелчок на имени файла в панели проводника приводит к открытию файла для редактирования . Допускается редак­ тировать несколько файлов одновремен- но, и вы можете создавать новые панели редактора, щелкая на кнопке S plit Edi t or (Разделить окно редактора) в правой вер­ хней части окна. Редактор Visual Studio Code вполне хорош , обладая неплохой поддержкой IntelliSense для файлов С# и представлений Razor, а также содействи­ ем в ;завершении имен и версий пакетов NuGet и Bower.
Глава 13 . Работа с Visual Studio Code 357 Кроме содержимого папки проекта панель проводника показывает. канне файлы в тенущий момент редактируются. Это позволяет легко сосредоточиться на подмножес­ тве файлов, с которыми производится работа, что является удобным дополнением, когда приходится иметь дело с поднабором связанных файлов в крупном проекте. Добавление в проект п акетов NuGet Первый шаг связан с добавлением пакетов NuGet, которые содержат сборки .NET, требующиеся для приложений MVC. В панели проводнина Visual Stu dio Code щелк­ ните на имени файла pro j ect . j son и с помощью редактора кода внесите в разделы dependencies и tools изменения, приведенные в листинге 13 . 1 . Редактор Visual Studio Cod e выдаст предположения относительно имен и версий пакетов . Ли стинг 1З . 1 . Добавление пакетов NuGet в файле proj ect . j s on " dependencies ": "Microsoft . NETCore . App " : "version": "1 .0 .0", "type " : "platforrn" ), "Microsoft .AspNetCore . Diagnostics" : "1 .0 .0 " , "Mi crosoft .AspNetCore . Se rver . IISintegrati on ": " 1 . 0 .0" , "Microsoft.AspNetCore .Server.Kestrel": " 1 . 0 . О ", "Microsoft . Extensions . Logging . Console": " 1 . 0 . 0 ", " Microsoft . Extensions . Conf i g u ra t ion.Enviro nrnentVa r iaЫes " : " 1.0 .0 ", "Microsoft . Extensions . Configuration . FileExten s i ons ": "1 . 0 . 0 ", "Microsoft.Extensi ons.Configurati on .Json": "1 .0 .0 ", " Microsoft . Extensions . Co nfiguration . ComrnandLine ": " 1 . 0 . 0 ", "Micr osoft.AspNetCore.Mvc" : 111 .0.011 , "Micro s oft . AspNetCo re.Static Fil es " : 11 1 .0.0 11 )' "tools": { "Microsoft .DotNet .Watcher. Tools " : 11 1.О.0-preview2-fi nal ", "Microsoft.AspNetCore . Server . I ISintegration . Tools" : "1 . 0 .0- previ ew2 - f i nal " }, Пакеты в разделе dependencies добавляют поддержку для МVС и доставки стати­ ческого содержимого , такого как файлы JavaScript и CSS. Пакет в разделе too ls де ­ лает возможной итеративную разработку в Visual Studio Code, которая настраивается в раздел е " Постро е ни е и запуск проекта" далее в глав е. Сохраните изменения в файле p roj e ct . j son и в окне командной строки /терми ­ нала выполните сл едующую команду. находясь внутри пашш Pa rt y i nvi t e s : dotnet res t ore Эта номанда обрабатывает файл pro j ect . j s on и загружает указанные в нем па­ кеты NuGet. ( Иногда редактор Visual Studio Code будет определять, что есть панеты, подлежащие загрузке . и предлагать выполнить такую команду, но на момент напи­ сания главы эт о средство было ненадежным, т. к. не все изменения обнаруживались . Явный запуск команды гарантирует, что приложение может быть скомпилировано . Перед запуском команды res t o r e может понадобить ся закрыть файл pro j ect . j s on в Visual Studio Code.)
358 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC Добавление в проект пакетов клиентской стороны Как и в Visual Studio 2015, при управлении пакетами клиентской стороны в про­ ектах Visual Studio Code применяется инструмент Bower, хотя для этого требуетсл до­ полнительнал работа. Первым делом необходимо добавить файл по имени . bowerrc , который исполь­ зуетсл длл того, чтобы указать Bower, где устанавливать пакеты. Наведите курсор мыши на элемент PARTYINVITES в панели проводника и щелкните на значке New File (Новый файл), как показано на рис. 13.7 . pro;et"tJsoп х "depe ndenc ies ·· : " ~\ic rosoft. NEТCore. Арр" : "version'': "1.0 .0 1 'J "type": "platform" ( 4 s 6 }' "M i crosoft . AspNetCore.Oiвgnostics": Рис. 13.7 . Создание нового файла Установите имл файла в . bowerrc (обратите внимание на наличие в имени двух букв r ) и поместите в него содержимое, приведенное в листинге 13.2 . Листинг 13.2 . Содержимое файла . bowerrc " directory ": " wwwroot/lib " Далее создайте файл по имени bower. j son с содержимым, представленным в лис­ тинге 13 .3 . Листинг 13.3. Содержимое файла bower. j son "name": "Party!nvites", "private": true, "dependencies": { "boo tstrap": " 3.3 .6", "jquery": "2 .2.3" , "j query-validation" : " 1. 15. О", " jquery - validation-unoЬtrusive ": " 3.2 .6 " При добавлении пакетов в файл proj ect. j son или bower. j son редактор Visual Studio Code обеспечивает поддержку IntelliSense , что облегчает выбор нужных паке­ тов и указание применяемых версий.
Глава 13. Работа с Visual Studio Code 359 Воспользуйтесь окном ком андной строки/т ерминала , чтобы выполнить в папке Partyinvi tes показанную ни же команду . которая применя ет инструмент Bower для загрузки и установки пакетов клиентской стороны, указанных в файле bower . j son : bower install Кон фигурирование приложения Процесс инициализации проекта создал пустой проект без поддержки МVС . В лис­ ти нге 13 .4 приведены и зменения , которые вносятся в файл Startup . cs для настрой­ ки МVС с использованием наиболее базовой конфигурации . Операторы в листинге применяют пакеты , которые были добавлены к проекту в листинге 13.1 и описаны в гл аве 14. Листинг 13.4 . Добавление поддержки MVC в файле Startup. cs using System ; using System . Collections . Generic ; using System . Linq ; using System . Threading . Tasks ; using Microsof t .AspNetCore . Bui lder ; using Microsoft . AspNetCore . Ho s ting ; using Microsoft . AspNetCore . Ht t p ; using Microsoft .E xtensions . Depe n dency i n j ection ; using Microsoft. Extensi ons . Logg i ng ; namespace Partyinvites { puЬlic class Startup { puЬlic void ConfigureServices( I ServiceCollection se r vices) { services.AddМvc(); puЫic void Configure(IApplicationBuilder арр , } IHos t ingEnvironmen t env, I Lo g ge rFacto r y logger Factory) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Построение и зап уск проекта Чтобы построить и запустить проект, в окне командной строки/терминала перей­ дите в каталог Pa r tyinvites и вьшолнит е следующую ком анду: dotnet watch run Редактор Visual Studio Code скомпилирует код в проекте и с помощью сервера при­ ложений Kestrel, рассм атриваемого в гл аве 14, запустит приложение, ожидая НТГР­ запросы на порте 5000. Любы е изменения в файлах С# будут инициировать авто­ м атич е скую п е рекомпиляцию. (Если вы хотите запустить про ект, игнорируя любые изменения. тогда исполь зуйте команду dotne t run .)
360 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Редактор Visual Studio Code не предоставляет аналога для средства Browser Link и не открывает окно браузера автоматичес 1ш. Чтобы протестировать приложение, от­ кройте окно браузера и перейдите на URL вида http: / / localhost: 5000. Вы полу­ чите ответ, представленный на рис. 13.8. Ошибка 404 связана с тем, что в текущий момент в проекте отсутствуют какие-либо контроллеры для обработки запросов. D localh ost:SOOO Х 1 ·-------·----·-·-· -·-, [·f- - ~- с l~,;_~~~"(}"_ ~~~~ ~-- -1 l::~~~~ode:~04~-~ot.~:~~~-------------·J Рис. 13.8. Тестирование примера приложе ния Воссоздание приложения Partylnvites Все подготовительные шаги завершены, а это значит, что мы можем заняться созда­ нием приложения МVС. Мы воссоздадим простое приложение Partylnvites из главы 2, но с рядом изменений и дополнений, которые подчеркнут особенности работы с Visual Studio Code. Создание модели и хранилища Первым делом наведите курсор мыши на элемент PARTYINVITES в панели про­ водника и щелкните на значке New Folder (Новая папка), как показано на рис. 13.9 . В качестве имени папки укажите Mo dels. EXPLOREI< ; OP.EN IOITORS pгoje•t,fien " PARТ YIN \ll ТfS ~ .vscode i• Proper ·ties t· W\WlfOO! 2 3 4 6 х "de pendenc ies'": "' r1 i crosoft. NEТCore. Арр" : ''version'' : '' 1.0.0'' , "'type'' : " platform" f ~ f }, ' "Hic1·o~oft. AspNetCoi-e. Diag11ostics" : \ 8 "M icrosoft . A~pHe'tCore . Server. IISI1>t,. , 9 "l-lic1·osoft. д,pNetCo r·e. Ser>rer. Kest•·y "!IF",.....1 1 1" " ' --ll!l!!!!lll[!Jl.,,,..,~,~.}J/J1iii1\~~on~o Рис. 13.9. Создание новой папки Щелкните правой кнопкой мыши на папке Models в панели проводника. вы­ берите в контекстном меню пункт New File (Новый файл), установите имя файла в GuestResponse. cs и поместите в файл код С# из листинга 13.5 .
Глава 13. Работа с Visual Studio Code 361 Работа с редактором Visual Studio Code Продукт Visual Studio Code (и расширение С#, установленное ранее в главе) предоставляет полнофункциональные средства для редактирования файлов С# и Razor, а также для файлов распространенных веб-форматов, подобных JavaScript, CSS и HTML. В принципе написание приложения MVC в Visual Studio Code имеет много общего с этим же процессом в редакторе Visual Studio 2015: имеется поддержка lntelliSense, кодирование цветом и подсветка ошибок (с советами по их исправлению) . Основной недочетом Visual Studio Code является отсутствие возможности настройки, осо­ бенно когда речь идет о форматировании кода. На момент написания главы были доступны варианты конфигурации для других языков, но расширение С# не допускает настройки, что несколько затрудняет работу, если предпочитаемый вами стиль написания кода не совпада­ ет со стилем, предлагаемым по умолчанию. Однако в целом редактор отличается быстрой реакцией и легкостью в применении, так что написание приложений MVC в среде OS Х/ macOS или Linux не вь1глядит второсортным процессом. Листинг 13.5 . Содержимое файла GuestResponse. cs из папки Models using System.ComponentModel.DataAnnotations; namespace Partyinvites . Models puЫic class GuestResponse { puЫic int id {get ; set; } [Required(ErrorMessage = " Please enter your narne")] 11 Пожалуйста , введите свое имя puЬlic string Narne { get; set; } [Required(ErrorMessage = " Please enter your ernail address " )] 11 Пожалуйста, введите свой адрес электронной почты [RegularExpression( ".+ \\@ .+ \\ .. +", ErrorMessage = " Please enter а valid email address")] 11 Пожалуйста, введите допустимый адрес электронной п очты puЫic string Emai l { get; set; } [Required(ErrorMessage = " Please enter your phone nurnber " } ] 11 Пожалуйста, введите свой номер телефона puЫic string Phone { get ; set ; } [Required(ErrorMessage = " Please specify whether you 'll attend " ) ] 11 Пожалуйста, укажите, примете ли участие puЫic bool? Wi llAttend { get; set; } Добавьте в папку Models файл по им е ни IReposi tory . cs с определением интер­ фейса. приведенным в листинге 13.6 . Самое важное отличие приложения в настоящей главе от приложения из главы 2 связано с тем. что мы собираемся хранить данные модели в постоянной базе данных. Интерфейс IReposi tory описьшает, 1щким обра­ зом прилож ение будет получать доступ к данным модели, не указывая реализацию. Добавьте в папку Models файл по имени ApplicationDbCon t ext . cs и определи­ те в нем класс контекста базы данных, как показано в листинге 13.7 .
362 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC Листинг 13.6. Содержимое файла IReposi tory. cs из папки Models us ing System.Col lections .Generic; namespace Partyinvites . Mod e l s { puЫic interface I Repository { IEnumeraЫe<Gues t Response> Responses {get ; void Ad dResponse(GuestRe sponse re spon se) ; Листинг 13.7 . Содержимое файла ApplicationDbContext. cs из папки Models usi ng Microsoft.EntityFrameworkCore ; namespace Part yinvit es . Mod e ls { puЬlic c l ass ApplicationDbContext DbCon tex t { puЬlic Ap p licationDbCon t ext() {} prot ected ove r ride void OnC onfi gur ing(DbCo nt extOptionsBuilde r buil der) builder . UseSqlite( " File name= . / Partyinvi tes . db " ) ; puЫic DbSet<GuestResponse> Invit es {get; set; } Средство SQLite храни т данные в файле, который указывается классом контекс­ та. В создаваемом примере приложения данны е будут храниться в файле по имени Partyi nvi tes . db , что определено в методе OnCon f iguring () . Чтобы завершить набор кл ассов, не обходи м ых для сохр анения и доступ а к данны м модели, тр ебуется реализация интерф ейса I Repo si tory, которая использует класс контекста базы данных. Добавьте в папку Mode l s файл по им ени EFReposi tory . cs и поместите в н е го код , представленны й в листинге 13.8 . Листинг 13.8. Содержимое файла EFReposi tory. cs из папки Models using Syst em.Coll ections .Generic; namespace Partyi nv i tes.Mode l s { puЫic cl ass EFRepository : IReposi tory private ApplicationDbContext context = new ApplicationDbContext() ; puЫic IEnumeraЬle<G u est R esponse> Responses => context . Invites ; p uЫic void AddRes p onse(GuestRe s pon s e re s ponse) { context .Inv i tes . Add(re s ponse) ; context . SaveChanges() ; Класс EFReposi tory следует шаблону, который похож на тот, что применялся в главе 8 Для настройки ба зы данных SportsStore . В листинге 13.9 видно, что к методу
Глава 13. Работа с Visual Studio Code 363 ConfigureServi ces () класса S t art u p добавлен оператор конфигурирования, кото­ рый сообщает инфраструктуре ASP.NET Core о необходимости созд ания экземпляра класса EFRepo si tory, когда требуются реализации интерфейса I Re p o si tory , с по­ м ощью ср едства вн едр ения зависимостей (рассматриваемого в главе 18). Листинг 13.9 . Конфигурирование хранилища в файле Startup.cs using System ; using System . Co l lections . Gener i c ; using System .Linq ; us i ng System.T h reading . Tasks ; us i ng Micro soft . AspNet Core . Builder ; using Mi crosoft . Asp NetCore . Host i ng ; using Microsoft . AspNetCore . Http; using Microsoft. Extensi ons . Depe n d encyi n j ection; using Micr osoft.Ext ensi ons . Loggi ng; using Partyinvites.Models; namespace Partyinvites { puЫic class Startup { puЫic void ConfigureServic e s(IServiceColle c tion servi ces) { services.AddTransient<IRepository, EFRepository>(); s e rvices.AddMvc() ; puЫic void Configu r e(IApplicat i onBui lder арр , IHostingE n vironme n t env , ILo g gerFac t ory l ogge r Factory) { app . UseStatusCode Page s () ; app.UseDevel operExceptionPage(); app .UseStaticFile s () ; app . UseMvcW i thDefa ul tRou t e() ; Создание базы данных В остальных главах книги всякий раз , когда нужно продемонстрировать функцио­ нальность, требующую постоянства данных , используется средство LocalDB. которое представля ет собой упрощенную версию Microsoft SQL Server. Но средство LocalDB до ступно только в среде Windows. поэтому при создании приложений ASP.NET Core MVC на других платформах понадобится какая-то альтернатива. Наилучшей альтер­ нативой LocalDB является SQLite - не нуждающ аяся в конфигурировании межплат­ форменная система управления базами данных, которая может встраиваться в при­ ложения и для которой компания Microsoft включила поддержку в Entity Framework Core . В последующих разделах мы рассмотрим процесс добавления SQLite в проект и ее примен е ни е в качестве хранилища данных для ответов на приглашения поучаст­ вов ать в в е черинке.
364 Часть 1. Введение в инфраструктуру ASP.NET Core MVC Использование SQLite при разработке Одна из причин того , что LocalDB является настолько полезным инструментом , связана с возмо жностью разработ ки с применением механизма баз данных SQL Server, который де­ лает переход в производственную среду SQL Server простым и почти полностью лишенным рисков. SQLite - великолепная база данны х , но она не очень х орошо подходит для крупно­ м асштабны х веб-приложений , а значит, что при развертывании пр иложения MVC требуется переход на другую базу данны х . Из м енения в конфигурации м огут быть упрощены с ис­ пользование м средств конфигурирования , которые будут описаны в главе 14, но приложе­ ние должно быть тщательно протестировано в испытательной среде, чтобы выявить любые отличия , вносимые производственной базой данных. Почитайте статью по адресу https : / /www . sqlite . org /whentouse . html , если вы не уверены в том, применять ли SQLite в производственной среде. Там приведен обзор ситуа­ ций, когда база данных SQLite может быть хорошим вариантом, а когда нет. Важно иметь в виду тот факт, что SQLite не поддер живает полный набор изменений схемы , которые Entity Framework Core может генерировать для других баз данны х. В целом это не проблема , к огда SQLite используется при разработк е, посколь ку вы можете удалить файл базы данных и сгенерировать новый файл с чистой схемой. Тем не менее, ситуация услож­ няется, если вы обдумываете развертывание приложения, в котором применяется SQLite. Если вы хотите использовать ту же самую базу данных в среде разработки и производс­ твенной среде , тогда ознакомьтесь со списком поддерживаемы х инфраструктурой Entity Framework Core баз данных по адресу http : //ef . readthedocs . io/en/latest/ providers/ index . htrnl. На момент написания главы список был коротким, но в Microsoft объявили о поддер жк е баз данных, которые больше, чем SQLite, подходят для процесса разработки и могут функционировать также на платформах, отличных от Windows . Добавление пакетов базы данных Первый шаг для любого нового средства в про екте ASP.NET Core пр едусматривает добавление обязательных п акето в к файлу proj ect . j son, и в Visual Studio Cod e оно ничем не отличается. В листинге 13.10 пока заны добавления к файлу project. j son для инфрастру~•туры Entity Framework Core и ее поддержки SQLite. Листинг 13.10. Добавление пакетов базы данных в файле project.json " dependencies ": " Microsoft . NETCore . App ": "version": "1.0.0 ", " type ": " platform " )1 " Microsoft . AspNetCore .D iagnostics ": " 1 . 0 . 0 ", " Microsoft . AspNetCore . Server .I ISintegration ": " 1.0 . 0 ", " Microsoft . AspNetCore . Server.Kestrel ": " 1.0 . 0 ", " Microsoft . Extensions.Logging.Console ": " 1 . 0 . 0 ", " Microsoft . Extensions . Configuration . EnvironmentVariaЫes ": " 1 . 0 . 0 ", " Microsoft . Extensions . Configuration . FileExtensions " : " 1.0 .0 ", " Microsoft . Extensions . Configuration . Json ": " 1 . 0 . 0 ", " Microsoft . Extensions.Configuration . CommandLine ": "1.0 .0", "Microsoft .AspNetCore.Mvc ": "1 .О . О", " Microsoft . AspNetCore . StaticFiles ": " 1.0 . 0 ",
Глава 13. Работа с Visual Studio Code 365 "Мicrosoft.EntityFrameworkCore.Sqlite": "1 .0 .0 ", "Мicrosoft.EntityFrameworkCore.Tools": "l .0.0 -preview2-final" ), "tools": { "Microsoft . DotNet . Watcher. Tools ": " l . 0 . 0 - preview2 -final", " Microsoft . AspNetCore . Server .I ISintegration .T ools ": " 1 . 0 . 0 - preview2 - fina l", "Мicrosoft.EntityFrameworkCore.Tools": { "v er sion": "1 .0 .0-preview2-final", " irnports": [ "portaЫe-net45+win8+dnxcore50", "portaЫe-net45+win8" ] )' Сохраните изменения в файле proj ect . j s on, откройте новое окно командной строки/терминала и выполните следующую команду в папке Partyinvi tes : dotnet rest ore Создание и применение миграции базы данных Создание базы данных следует похожему проц ессу в смысле команд. используемых средой Visual Studio 2015, хотя выполняется с применением инструмента dotnet. Чтобы создать начальную миграцию базы данных, выполните показ анную ниже ко­ манду, находясь в папке Partyinvi t es: dotnet ef migrations add In itial Инфраструктура Entity Framework Core создаст папку по имени Mig r at i ons , содержащую файлы классов С#, которые будут использоваться для настройки схе­ мы базы данных. Для применения этой миграции базы данных запустите в папке Partyinvi tes приведенную далее команду, которая создаст базу данных в папке Ьin/Debug/netcoreappl . O: dotnet ef database update Редактор Visual Studio Code не включает поддержку для инспектирования баз дан­ ных SQLite, но на веб-сайте http: / / sqli t ebrowser . org можно найти великолеп­ ный инструмент с открытым кодом для Windows, OS X/macOS и Linux. Создание контроллеров и представлений В этом разделе мы добавим в приложение контроллер и представления. Начнит е с создания папки Controllers и добавьте в нее файл класса по имени HomeControll er. cs с определением из листинга 13 .11 . Совет. Создание пап ки в редакторе Visual Studio Code может вызвать затруднения, т.к. щел­ чок на элементе PARTYINVITES в панели проводника скрывает содер жим ое папки, а не выбирает корневую папку. Щелкните на одном из файлов в корневой папке, таком как proj e ct. j son , наведите курсор мыши поверх элемента PAR TYINVITES и щелкните на значке New Folder.
366 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC Листинг 13.11. Содержимое файла HomeController. cs из папки Controllers using System; using Microsoft . AspNetCore . Mvc ; using Partyinvites.Models ; using System . Linq; namespace Pa r tyinvites . Controllers puЫic c l ass HomeC o ntroller : Controller p ri vate IRe pository repository ; puЫic HomeController(IRepository repo) { this . repository = repo ; puЬlic ViewResu l t Index() int hour = DateTime . Now . Hour ; ViewBag. Greeting = hour < 12 ? "G ood Morning " return View ( "MyView " ) ; " Good Afternoon"; [HttpGet] puЫic ViewResult RsvpForm() { return View() ; [H ttpPost ] puЫic ViewResult RsvpForm(GuestResponse guestResponse) if (ModelState . IsValid) { repository.AddResponse(guestResponse); return View( " Thanks ", guestResponse) ; else{ //Имеется ошибка проверки достоверности return View(); puЬlic ViewResult ListResponses() { return View(repository . Responses . Where(r => r . WillAttend true)) ; Чтобы установить встроенные дес1<рипторные вспомогательные классы, создайте папку Vi ews и добавьте в не е файл по имени_Viewimports . cshtml, который содер ­ жит выражение, показанное в листинге 13.12. Листинг 13.12. Содержимое файла_Viewimports. cshtml из папки Views @addTagHelper * , Microsoft . AspNetCore.Mvc . TagHelpers Далее создайте папку Views/Home и добавьте в нее файл по имени MyView . cshtml, в котором определяется представление , выбираемое методом действия Index () из листинга 13.11. Пом естите в него разметку, приведенную в листинге 13 . 13.
Глава 13 . Работа с Visual Studio Code 367 Л истинг 13.13. Содержи м ое файла МyView. cshtml из папки Views/Home @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name= " v i ewport " content= " width=device-width " /> <title>Index</title> <link rel= " stylesheet " href= " / liЬ/bootstrap/dist/ css/bootstrap . css " /> </head> <body> <div class="text - center " > <hЗ>We're going to have an exciting party!</hЗ> <h4>And you are invited</h4> <а class= " Ьtn Ьtn - primary " asp - act i on= " RsvpForm " >RSVP Now</a> </div> </body> </html> Добавьте в папку Views/Home файл по имени RsvpForm . cshtml с содержимым, показ анным в листинге 13. 14 . Это пр едставление пр едлагает НТМL- форму , которая будет заполняться по льзователем , чтобы принять или отклонить пригл ашение на вечеринку. Листинг 13.14. Содержимое файла RsvpForm. cshtml из папки Views/Home @model Partyinvites . Models . GuestResponse @{ Layout = null ; <!DOCTYPE htm l > <html> <head> <meta name =" viewport " content=" width=device - width " /> <title>RsvpForm</title> <link rel= " stylesheet " h ref= " /liЬ/bootstrap/dist/css/bootstrap . css " /> </head> <body> <div class =" panel panel - success " > <div class= " panel - heading text - center " ><h4>RSVP</h4></div> <div class= " panel - body " > <form class="p - a - l " asp- action="RsvpForm" method=" post"> <div asp - validation - summary= " Al l" ></div> <div class= " form - group " > <label asp-for= " Na me " >Your name : </label> <input class= " form - control " asp - for= " Name " /> </div> <div class= " form- group " > <l abe l asp - for= " Email " >Your emai l: </label> <i nput class= " form- control " asp - for= " Email " /> </div>
368 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC <div class="form-group"> <label asp - for="Phone " >Your phone : </label> <input class= " form - control" asp - for= " Phone" /> </div> <div class= " form- group"> <label>Will you attend?</label> <select class= " form-control" asp - for= " WillAttend"> <option value="">Choose an option</option> <option value= " true " >Yes, I'll Ье there</option> <option value="false " >No, I can ' t come</option> </select> </div> <div class="text- center " > <button class= " Ьtn Ьtn-primary" type="submit"> Submi t RSVP </button> </div> </form> </div> </div> </body> </html> Следующий файл представления называется Thanks . cshtml, также создается в папке Views/Home и имеет содержимое, приведенное в листинге 13.15, которое отоб­ ражается, когда гость отправляет свой ответ. Листинг 13.15. Содержимое файла Thanks. cshtml из папки Views/Home @model Partyinvites.Models . GuestResponse @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device - width" /> <title>Thanks</title> <link rel= " stylesheet" href=" / liЬ/bootstrap/dist/ css/bootstrap. css" /> </head> <body class= " text-center"> <р> <hl>Thank you , @Model.Name!</hl> @if (Model . WillAttend == true) { @:It's great that you 're coming . The drinks are already in the fridge! else { @:Sorry to hear that you can ' t make it, but thanks for letting us know. </р> Click <а class= " nav-link" asp - action="ListResponses">here</a> to see who is coming. </body> </html>
Глава 13. Работа с Visual Studio Code 369 Финальный файл представления имеет имя ListResponses. cshtml и подобно другим представлениям в примере добавляется в папку Views/Home. Он отображает список ответов гостей, используя разметку из листинга 13.16. Листинг 13.16. Содержимое файла ListResponses. cshtml из папки Views/Home @model IEnumeraЬle<Partyinvites . Models.GuestResponse> @{ Layout = null; <!DOCTYPE html> <html> <head> <meta nam e= " viewport " content="width=device - width " /> <link rel=" stylesheet" href=" /liЬ/bootstrap/dist/ css /bootstrap . css " /> <title>Responses</title> </head> <body> <div class= "pane l-body " > <h2> Here is the list of people attending the party</h2> <tаЫе class= "t aЫe taЬle-sm taЫe-striped taЬle -b ordered " > <thead> <t r><th>Name</th><th>Email</th><th>Phone</th></tr> </thead> <tbody> @foreach (Partyinvites .Mod els .Gues tResponse r in Model) { <t r> <td>@r . Name</ td><td>@r . Email</td><td>@r.Phone</td></tr> } </tbody> </tаЫе> </div> </body> </html> Запущенная ранее в главе :команда dotnet watch гарантирует :компиляцию прило­ жения всякий раз, когда редактируется какой -либо 1ишсс С#, и вы можете просмотреть завершенное приложение, перейдя на URL вида http: / /localhost: 5000 (рис. 13.10). Модульное тестирование в Visual Studio Code Редактор Visual Studio Code не поддерживает отдельные проекты модульного тестирования, а это значит, что модульные тесты должны смешиваться с класса­ ми приложения MVC и :конфигурироваться с применением того же самого файла proj ect . j son, с помощью :которого настраиваются пакеты и инструменты ASP.NET . В последующих разделах мы добавим к приложению пакет тестирования xUnit, созда­ дим простой модульный тест и прогоним его. Конфигурирование приложения Первый шаг предусматривает добавление пакетов в файл proj ect . j son и указа­ ние деталей об используемом пакете тестирования (листинг 13.17).
370 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC f- _,с -i RSVP i We're going to have an exciting ' v,.,, ~"''" 1 And you ~re lnv1ted А 8"" .__.,,,, 1 Уощоmа!1 · !...... _ __ _ --~---- _______) ЬoЬO-><x<i"np\ecom 1 Уаи рlюо•' 1.~ L Thank you , ВоЬ! n'.s grмt ll'чll you'm COПllПfJ Th~ dМk.1 а1е а1w..гау ln the !rdgo~ CllCk tюн1 ~ sве who lз oom!l'W]. ~- -- -· С !_Ф I0(.1t :t-:.O<X.1/H~1"f::1r_R11~;..ti1•'~ __ 1 1, \V:~:: 7 :ood? Неге is th e li st of people attending t11e рагtу Namo μr\Of}Q Yll!i.1 "000\h& ro 55.>1234 ''**'' 5S!:o--(;789 Рис. 13.1 О. Выполнение завершенного приложения Листинг 13.17. Конфигурирование модульного тестирования в файле proj ect. j son " dependencies ": { " Microsoft.NETCore . App ": "version": "1 .0 .0", "type": " platform " )1 " Microsoft.AspNetCore . Diagnostics ": " 1 . 0 . О ", " Microsoft.AspNetCore . Server .II Sintegration ": " 1 . 0.0 ", "Microsoft.AspNetCore.Server.Kestrel": "1.0.О", " Microsoft .E xtensions . Logging .C onsole ": "1. 0 . О ", " Microsoft . Extensions . Configuration.EnvironmentVariaЬles ": "1.0.0", " Microsoft . Extensions . Configuration.FileExtensions" : "1. 0.О ", " Microsoft . Extensions . Configuration . Json ": " 1 . 0 . 0", " Microsoft . Extensions . Configuration . CommandLine" : " 1 . 0 . О ", " Microsoft . AspNetCore . Mvc ": " 1.0.0 ", " Microsoft . AspNetCore . StaticFiles ": "1.0.О", " Microsoft .En tityFrameworkCore . Sqlite ": " 1 . 0 . О ", " Microsoft . EntityFrameworkCore.Tools ": " l . 0 . 0-preview2-final ", "xunit": "2 .1 .0 " , "dotnet-test-xunit": "2 .2 . 0 -preview2-build1029" )1 "t estRunner ": " xunit ", "tools": { }1 " Microsoft.DotNet.Watcher . Tools ": " l . 0 . 0 -preview2 - final ", " Microsoft . AspNetCore . Server . IISintegration.Tools ": "l.0.0-preview2-final", " Microsoft.EntityFrameworkCore .T ools ": { " version ": "l. 0 .0 - preview2-final ", " imports ": [ " portaЫe -n et45+win8+dnxcore50" , " portaЫe -n et45+win8" ] / / .. . для краткости остальные разделы не показаны ...
Глава 13. Работа с Visual Studio Code 371 Выполните в папке Partyinvi tes следующую команду для установки пакетов тестирования: dotnet restore Создание модульного теста Модульные тесты создаются так, как было описано в главе 7, но тестовые н:лас­ сы должны быть частью проекта приложения. Создайте папку Tests и поместите в нее файл класса по имени HorneControl lerTests. cs с содержимым, приведенным в листинге 13.18. Листинг 13.18. Содержимое файла HomeControllerTests. cs из папки Tests using System; using System . Co llecti ons . Generic ; using Partyinvites . Controllers; using Partyinvites.Models ; using Xunit ; using Microsoft.AspNetCore.Mvc; using System.Linq ; namespace Partyinvites . Tests puЫic class HomeControllerTests [Fact] puЬlic void ListActionFi l tersNonAttendees() 11 Организация HomeController controller = new HomeController(new FakeRepository()) ; 11 Действие ViewResult result = controller . ListResponses() ; 11 Утверждение Assert . Equal(2 , (result . Model as IEnumeraЬle<GuestResp onse>) . Count()); class FakeRepository : IRepository { puЫic IEnumeraЬle<GuestResponse> Responses => new List<GuestResponse> { new GuestResponse { Name new GuestResponse { Name new GuestResponse { Name }; " ВоЬ ", WillAttend = true } , "Alice", WillAttend = true }, " Joe ", WillAttend = false } puЫic void AddResponse(Gues tRespons e re sponse ) t hrow new NotimplementedException() ; Это стандартный тест xUnit, который проверяет действие Li stResponses в конт­ роллере Home и фильтрует в хранилище объекты GuestResponse со значением свойс­ тва WillAttend, равным false. Прогон тестов Редактор Visual Studio Code обнаруживает тесты и добавляет встроенную ссылку для их з апус1ш (рис . 13.11).
372 Часть 1. Введение в инфраструктуру ASP.NET Core MVC [Fact) О refere rкeJ 1 run te st !Jiebug test puЬlic v___ """ •..• tionFiltersNonAttendees() { } //Arrange HomeController controller = new HomeController(ne1~ FakeReposi tory()); // Act ViewResult result = controller.ListResponses(); 11 Assert Assert.Equal(2, (result.Model as IEnumeraЬle<GuestResponse>).Count()); Рис. 13.11 . Прогон теста внутри редактора кода Щелчок на ссылке run test (прогнать тест) приведет к открытию окна вывода и отображению результата . (На момент написания: главы ссылка debug test (отладить тест) не работала, а в ряде случаев вывод мог вообще не отображаться.) Более надежный подход предусматривает прогон всех тестов в проекте. Выполните следующую команду в папке прое1па: dotnet test Все тесты в проекте запустятся, и в результате отобразится вывод, подобный по ­ казанному ниже: xUnit . net .NET CLI test runner (64 -b it . NET Core win10-x64) Discovering : Partyinvites Discovered : Partyinvites Starting : Partyinvites Finished: Partyinvi tes === TEST EXECUTION SUMMARY === Partyinvites Total : 1, Errors : О, Failed : О, Skipped: О , Time : 0.196s SUMMARY: Total: 1 targets, Passed: 1, Failed: О . Прогнать все модульные тесты в проекте после каждого изменения классов С # можно также с помощью следующей команды: dotnet watch test Такая команда не может применяться одновременно с командой dotnet wa tch run, поскольку обе команды будут инициировать компиляцию проекта при наличии изме­ нений и пытаться создавать те же самые выходные файлы. Резюме В этой главе был предложен краткий обзор работы с редактором Visual Studio Code - легковесным инструментом разработки , который поддерживает создание при­ ложений ASP.NET Core МVС в средах Windows, OS X/macOS и Linux. Редактор Visual Studio Code пока еще не является заменой полного продукта Visual Studio , но предо­ ставляет основные средства, которые необходимы при построении приложений MVC , и расширяется компанией Microsoft в ежемесячных выпусках. Итак, первая часть книги завершена. Во второй части мы начнем погружени е в детали и посмотрим, как работают средства, которые использовались для создания приложения.
ЧАСТЬ 11 Подробные сведения об инфраструктуре ASP.NET Core МVС к настоящему моменту вы уже знаете, для чего существует инфраструкту­ ра .NET Core MVC , а также понимаете ее архитектуру и лежащие в основе проектные цели. Вы получили эти знания благодаря построению реалистичного приложения электронной коммерции. Наступило время заняться исследованием внутреннего устройства инфраструктуры. Во второй части книги мы погружаемся в детали. Мы начнем с изучения струк­ туры приложения ASP.NET Core MVC и способа обработки запросов . Затем мы сосредоточим внимание на индивидуальных средствах, таких как маршрутиза­ ция, контроллеры и действия, система представлений и дескрипторных вспомо­ гательных классов, а также особенности работы MVC с моделями предметной области.
ГЛАВА 14 Конфигурирование v приложении тема конфигурации может показаться неинтересной, но она открывает много ас­ пектов , связанных с функционированием приложений МVС и обработкой НТГР­ запросов. Вы не должны поддаваться соблазну пропустить эту главу. Вы обязаны вы­ делить время на изучение способа, которым система конфигурации придает форму веб - приложениям MVC . Данная тема заслуживает внимания и формирует прочный фундамент для понимания материала последующих глав. Если вы работали с предшествующими версиями ASP.NET, то увидите, что од­ ним из наиболее заметных изменений в ASP.NET Core является способ, с помощью которого приложение конфигурируется. Исчез целый набор файлов - Glob al . asax. Filt e rConfig. cs и Rou teConfig. cs, - а на его место пришли классы St artup и Prograrn плюс комплект файлов JSON. В настоящей главе объясня ется , как все это применять для конфигурирования приложений МVС, и демонстрируется, каким об­ разом инфраструктура МVС опирается на средства. предоставляемые платформой ASP.NET Core. В табл. 14. l приведена сводка, позволяющая поместить конфигуриро­ вание приложений в контекст. На заметку! В Microsoft объявили, что в будущем выпуске изменят способ конфигурирова­ ния приложений ASP.NET Core, скорректировав роль файла pro j ect. j son и представив ХМL-файл конфигурации. Проверяйте веб-сайт издательства на предмет обновлений, ко­ торые появятся после выпуска новых инструментов. Таблица 14. 1. Помещение системы конфигурации в контекст Вопрос Что это такое? Чем она полезна? Как она используется? Ответ Классы Pr o gram и Startup и файлы JSON используются для конфигурирования работы приложения, а также указания паке­ тов , от которых оно зависит Система конфигурации позволяет подстраивать приложения под их среды и управлять зависимостями от пакетов Самым важным компонентом является класс Startup, который применяется для создания служб (объектов, предоставляющих общую функциональность повсюду в приложении) и компонентов промежуточного программного обеспечения (ПО) , используемых для обработки НТТР-запросов
Глава 14 . Конфигурирование пр и ло ж ен и й 375 Окончание табл. 14. 1 В опрос Ответ Существуют ли к а ки е- то скрытые ловушки или ограничения? В сло жны х прило жениях конфигурация может стать трудной в управлении. В разделе " Работа со сложными конфигурациями" далее в главе описаны средства ASP.NET, предназначенные для решения та кой проблемы Существуют ли альтернативы? Нет. Систе м а конфигурации - это неотъе мле м ая часть ASP.NET и средство настрой ки приложений MVC Из м енилась ли она по сравнению с версией мvс 5? Система конфигурации полностью изменилась по сравнению с той , которая существовала в версии MVC 5, и поддерживает со­ вершенно новый подход , предназначенный для того , чтобы сде­ лать возмо жным функционирование приложений за пределами традиционно й платформы llS В табл. 14 .2 представлена сводка по главе . Таблица 14 .2. С водка по главе Задача Добавлени е фун кциональности в приложение Управление инициал и зацией приложения ASP.NET Конф и гурирование прило жен и я Создание обще й фун кциональности Генерация ответов с содерж имым Предотвращен и е прохождения за­ просов через конве йер запросов Редактирование запроса перед его обработ кой другими компонента м и про м ежуточного ПО Редактирование ответа, который был обработан другим и компонен­ тами промежуточного ПО Настрой ка фун кциональности MVC Изменение конфигураци и прило же­ ния для разны х сред Рег и страция данны х приложения в журнале Обработ ка ошибо к в приложении Решен ие Добавьте па кеты NuGet в разделы dependen cies и tools файла proj ect . j son Используйте класс Program Применяйте методы ConfigureServices () и Configure () класса Startup Используйте метод ConfigureServices () для создания служб Создайте проме жуточное ПО для генера­ ции содер ж имого Создайте проме жуточное ПО для обхода Создайте проме жуточное ПО для редакт и рования запросов Создайте проме жуточное ПО для редактирования ответов Применяйте метод UseMvc () или UseMvcWithDefaultRoute() Используйте службу среды размещения Применяйте промежуточное ПО для регистрации в журнале Используйте промежуточное ПО для обра­ ботки ошибок в среде разработки или про ­ изводственной среде Листинг 14. 1- 14.6 14.7 14 .8, 14.9 14.10- 14. 12 14.13- 14. 15 14. 16- 14. 17 14.18- 14 .20 14.21 , 14.22 14 .23 14 .24 14.25- 14.27 14.28, 14 .29
376 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Окончание табл. 14.2 Задача Решение Листинг Управление несколькими браузера- Применяйте средство Browser Liпk (Ссыл ка 14.30 ми во время разработки на браузер) Включение файлов изображений , Включите проме жуточное ПО для статичес- 14.31 JavaScript и CSS кого содер жим ого Отделение данных конфигурации от Создайте внешние источники конфигура- 14.32- кода С# ции, такие как файлы JSON 14.37 Конфигурирование служб MVC Используйте средства параметров 14 .38 конфигурации Конфигурирование сло жн ых Применя йте несколько внешн их файлов или 14.39- прило же ний классов 14.43 Подготовка проекта для примера В этой главе мы создаем новый проект по имени ConfiguringApps с использова­ нием шаблона Empty (Пустой). Мы будем конфигурировать приложение позже в главе, но есть несколько базовых средств, которые необходимо поместить на свои места в плане подготовки к предстоящим изменениям. Мы собираемся применять Bootstrap для стилизации НТМL-содержи м ого, поэтому создайте файл bower . j son, используя шаблон элемента Bower Configuration File (Файл конфигурации Bower), и добавьте в него пакет, как пшшзано в листинге 14.1 . Листинг 14.1. Добавление Bootstrap в файле bower. j son " name ": " asp. net", "p rivate ": true , " dependencies ": { "bootstrap": "3. 3 . 6" Создайте папку Controllers и поместите в нее файл класса по имени HorneController. cs с опред еле нием контроллера из листинга 14.2. Листинг 14.2 . Содержимое файла HomeController. cs из папки Controllers using Systern . Collections.Generic; using Microsoft.AspNetCore . Mvc ; namespace ConfiguringApps.Control l ers puЬlic class HomeController : Controller puЫic ViewResult Index() => View(new Dictionary<string , string> ["Message" ] = "This is t he Index action" });
Глава 14. Конфигурирование приложений 377 Создайте папку Vi ews/Horne и добавьте в нее файл представления по имени Index . cshtrnl с содержимым, приведенным в листинге 14.3 . Листинг 14.З. Содержимое файла Index.cshtml из папки Views/Home @model Dictionary<string, string> @{ Layout = null; } < !D OCTYPE htrnl> <html> <head> <meta name= " viewport " con tent=" wi dth=device -width " /> <link asp- href- include= " liЬ/boo tstrap / dis t/css/* . min. css " rel ="stylesheet" /> <title>Res ul t</ title> </head> <body class= "panel-body" > <tаЫе c l ass= " ta Ыe taЫe-condensed taЬle-bord ered taЬle-striped" > @foreach (var kvp in Model) { <tr><th>@kvp .Key</th><td>@kvp .Value</td></tr> </tаЫе> </body> </html> Элемент link в представлении полагается на встроенный дескрипторный класс для выбора СSS-файлов Bootstrap. Чтобы вилючить встроенные дескрипторные илас­ сы, создайте в папке Views файл_Viewirnports. cshtrnl с применением шаблона элемента MVC View lmports Page (Страница импортирования представлений MVC) и поместите в него выражение, показанное в листинге 14.4 . Листинг 14.4 . Содержимое файла_Viewimports. cshtml из папки Views @addTagHelper * , Microsoft .AspNe t Core .Mvc.TagHelpers На заметку! В настоящий момент приложение не компилируется и не запускается, поскольку контроллер и представление зависят от средств, которые в проекте пока еще отсутствуют. Проблема будет решена в последующих разделах при рассмотрении способа конфигури­ рования приложений ASP.NET Core MVC. Конфигурационные файлы JSON При разработке приложений ASP.NET Core формат JSON (JavaScript Object Notation - система обозначений для объектов JavaScript) играет две разных роли. Первая из них заилючается в том, что это предпочтительный формат для обмена дан­ ными между приложением MVC и его илиентами. В главе 20 будет показано, как со­ здавать контроллеры. которые возвращают своим клиентам данные в формате JSON вместо HTML, что делает возможными асинхронные НТГР-запросы для извлечения только данных, необходимых илиентам. Обычно такие запросы называют запросами Ajax, хотя JSON в значительной степени заменил формат ХМL, который символизи­ рует буква "х" в Ajax.
378 Часть 11 . Подробные сведения об инфраструктуре ASP.NEТ Core MVC В текущей главе нас интересует вторая роль формата JSON, связанная с тем, что он является форматом для конфигурационных файлов. В табл. 14.3 описаны разно­ образные конфигурационные файлы JSON, которые могут быть добавлены к прило­ жению ASP.NET Core MVC. Совет. Формат JSON используется для описания сериализированных объектов и не подде­ рживает какую-либо программную логику. В противоположность этому файлы с расши­ рением . j s содержат код JavaScript, который может быть выполнен. Файлы JSON не в состоянии содержать код JavaScript, но файлы JavaScript могут включать данные JSON (т.к . формат JSON основан на способе определения литеральных объектов JavaScript) . Таблица 14.З. Конфигурационные файлы JSON в проекте ASP.NET Core MVC Имя global .j son launchSettings.json appsettings . json bower . json bundleconfig . json project . json project . lock . json Описание Этот файл, находящийся в папке элементов решения, от­ вечает за сообщение среде Visual Studio о том, где искать проекты внутри решения и какая версия исполняющей среды .NET должна применяться для запуска приложения . За более подробными сведениями обращайтесь в раздел "Конфигурирование решения" далее в главе Этот файл, который отображается раскрытием элемента Proper ties в проекте приложения MVC, используется для указания, каким образом запускается приложение Этот файл применяется для определения настроек, специфич ­ ных для приложения , как описано в разделе " Использование данных конфигурации" далее в главе Этот файл применяется инструментом Bower для перечисле­ ния пакетов клиентской стороны, которые установлены в про­ екте, как объяснялось в главе 6 Этот файл используется для пакетирования и минификации файлов JavaScript и CSS files, как было описано в главе 6 Этот файл применяется для указания пакетов NuGet, установ ­ ленных в приложении, как объяснялось в главе 6. Он также используется для других настроек проекта, как показано в разделе " Конфигурирование проекта" далее в главе Этот файл, который отображается раскрытием элемента proj ect. j son в окне Solutioп Explorer, содержит детали­ зированные зависимости между пакетами, установленными в проекте. Он генерируется автоматически и не должен редак­ тироваться вручную Конфигурирование решения Файл globa l. j son применяется для конфигурирования р ешения в целом. Вот со­ держимое, которое Visual Studio по умолчанию добавляет для проекта ASP.NET Core:
"projects": [ "src", " test" ], "sdk": { Глава 14 . Конфигурирование приложений 379 " version ": "1.0 .0 -preview2-003121" Настройка proj ects указывает набор папок. которые содержат проекты л ибо ис­ ходный код. По соглашению развертываемая часть решения - например, приложе­ ние МVС - по ме щается в папку src, тогда как проекты тестирования сохраняются в папке test . Это только соглашение, и вы можете использовать файл global . j son для перечисления любого желаемого набора папок и применять его для любой цели, которая вам подходит. Настройка sdk сообщает среде Vlsual Studio о том, какая вер­ сия .NET будет использоваться для запуска проекта. Версия. указанная в этой на­ стройке, прим еняется для всех проектов в решении. Формат JSON: кавычки и запятые Если вы ранее не работали с форматом JSON, тогда полезно посвятить некоторое время чтению спецификации на веб-сайте www. j son. org. С форматом JSON легко оперировать, к тому же большинство платформ предлагают хорошую подцержку для генерации и разбо­ ра данных JSON, в числе которых и приложения MVC (примеры ищите в главах 20 и 21 ) , а на уровне клиента используется простой АРl - интерфейс JavaScript . В действительности большая часть разработчиков приложений MVC вообще не будут взаимодействовать с JSON напрямую, а написание кода JSON вручную требуется только в файлах конфигурации . Существуют две ловушки, в которые попадают многие разработчики, ранее не имеющие дела с JSON. Хотя вы по-прежнему должны найти время на чтение спецификации, знание наиболее распространенных проблем предоставит вам определенную отправную точку в ситуации, когда Visual Studio или ASP.NEТ Core не смогут провести разбор ваших файлов JSON . Ни ж е показано добавление к стандартному содержимому файла global . j son , в котором присутствуют две самых распространенных проблемы (оно приводится только в демонстрационных целях, потому что в случае добавления таких записей в global . j son среда Visual Studio сообщит об ошибке): "projects": [ "src", "test" ], " sdk": { " version ": " l . 0 . 0 -preview2-003121 " mysetting : [ fast, slow ] Во-первых, едва ли не все данные в JSON помещаются в кавычки. Очень легко забыть , что вы пишете вовсе не код С#, и посчитать, что имена свойств и значения должны быть вос­ приняты без кавычек. Тем не менее, в JSON все кроме булевских значений и чисел должно помещаться в кавычки, например: "projects": [ "src", "test" ] , " sdk": { "version": " l .0.0-preview2- 003121" "mysetting" : [ "fast", "slow"]
380 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Во - вторых, при добавлении нового свойства к JSОN-описанию объекта вы должны помнить о необходимости добавления запятой после предшествующего символа фигурной скобки: "projects": [ "src", "test" ], " sdk": { "version" : "l .0.0 -preview2-003121" }' "mysetting" : [ "fast ", " slow"] Заметить разницу довольно трудно даже в случае ее выделения полужирным (именно пото­ му такая ошиб ка является распространенной), но здесь помещена запятая после символа } , закрывающего раздел sdk. Одна ко будьте внимательны , потому что замыкающая запятая , после которой отсутствует раздел, также приводит к ошибке. Если внесенные вами изме ­ нения в файл JSON вызвали проблемы, то в первую очередь нужно выполнить проверку на предмет двух указанны х выше ошибо к. Конфигуриро в ание п роекта Файл proj ect . j son прим еняется для конфигурирования одиночного про ект а внутри р ешения. Вот стандартное содержимое файла pro j ect . j son для прило жения MVC, которое со здается шаблоном Empty: " dependencies ": { "Microsoft.NETCore .App ": "version": "1 .0 .0", " type ": " platform " )1 }1 "Microsoft.AspNetCore.Diagnostics": "1 .0 .0", " Microsoft . AspNetCore . Server .II Si ntegration ": "1.0 .0", " Microsoft . AspNetCore . Server . Kestrel ": "1 .0 . 0 ", " Microsoft . Extensions . Logging.Console ": " 1 . 0 . 0 " "tools": { " Mi crosoft . AspNetCore . Server . IISintegration.Too l s ": " l .0 .0-preview2-final" )' " frameworks ": { " netcoreappl . 0 ": "imports ": ["dotnet5.6 ", " po r taЬle - net45+wi n 8 " ] )1 " bui l dOptions ": { " emitEntryPoi nt ": true , " preserveCompilationContext ": tr ue } , " runtimeOptions ": { )1 " configProperties ": { " System.GC .Server ": true
Глава 14 . Конфигурирование прило ж ений 381 11 puЫ i shOptions 11 : { 11 incl ude 11 : [ 11 wwwroot 11 , 11 web. confi g11 ] )' 11 scripts ": { 11 postpuЫish11: [ 11 dotnet puЬlish-iis --puЫish-folder %puЫish:OutputPath% --framewor k % puЫish : FullTargetFramewo r k % 11 ] } В табл. 14.4 описаны все разделы конфигурации в файл е p roj ec t. j son . Двумя наиболее важными частями файла proj ect . j s on являются d e pendenc i es и too ls, которые будут обсуждаться в последующих разделах. Таблица 14.4. Разделы конфигурации в файле project. json Имя dependen cies tool s framewo r ks buildOptions runtirneOptions puЫishOptions scri pts Описание В этом разделе указываются пакеты NuGet, от которых зависит проект, как объясняется ниже в разделе "Добавление зависимостей в файл project.j son" В этом разделе настраиваются пакеты, которые используются в качес­ тве инструментов разработки, как показано в разделе "Регистрация инструментов разработки в файле p r oj ect . j son" далее в главе В этом разделе указываются инфраструктуры .NET, на которые ориен­ тирован проект, и требуемые ими зависимости Этот раздел применяется для конфигурирования способа построения проектов Этот раздел используется для конфигурирования способа запуска приложения Этот раздел применяется для конфигурирования способа опубликова­ ния проекта В этом разделе указываются команды, которые выполняются в ключе­ вые моменты жизненного цикла построения, такие как момент перед развертыванием приложения Добавление зависимостей в файл project. json Раздел depende n c ies относится к тем разделам файла, которые будут редактиро­ ваться наибол ее часто по мере добавления пакетов , которые предоставляют функцио­ нальность, требуемую в проекте. В листинге 14.5 добавлен набор пакетов, предостав­ ляющих основные средства , которые полезны при разработке приложений MVC. Листинг 14.5. Добавление пакетов, полезных при разработке приложений MVC, вфайлеproject.j son 11 dependencies 11 : { 11 Microsoft . NETCore . App 11 : 11 versi on11 : 11 1.0 .0 11 , 11 typ e 11 : 11 pla tform 11 },
382 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC }/ " Microsoft . AspNetCore . Diagnos tics ": "1. О . О ", "M icrosoft . AspNetCore . Server.IISintegration ": "1. 0 . О ", " Microsoft . AspNetCore .S erver .K estrel": "1.0. 0 ", " Microsoft . Extensions . Logging . Console ": "1. 0 . 0 ", "Microsoft.AspNetCore.StaticFiles": 11 1.0 .0 11 1 "Microsoft.AspNetCore .Mvc": "1. О. 0", "Microsoft.VisualStudio.WeЬ.BrowserLink.Loader": 11 14.0 .0", "Microsoft.AspNetCore.Razor.Tools": "version": "1. О. O-preview2-final", "type": "build" Для большинства пакетов можно использовать простой синтаксис, который пре­ дусматривает указание имени пакета и требуемой версии: " Microsoft . AspNetCore . Mvc " : "1. О . О", Расширенный синтаксис позволяет указывать тип зависимости, что будет оказы­ вать воздействие на способ ее применения . В файле proj ect. j son присутствуют два случая использования расширенного синтаксиса, включая следующий: " Microsoft.NETCore . App ": "version": "1.О.О", " type ": "pl atform " }, В свойстве version указывается номер выпуска пакета точно так же, как в про­ стом синтаксисе. В свойстве type предоставляется дополнительная информация о роли пакета. Здесь допускается указывать одно из трех значений, которые описаны в табл. 14.5 . Таблица 14.5 . Значения свойства type в расширенном синтаксисе описания зависимостей в файле project. json Имя defaul t platform build Описание Это значение указывает обычную зависимость разработки, так что прило­ жение при выполнении своей работы полагается на сборки в пакете. Такое значение применяется в простом синтаксисе Это значение указывает, что пакет предоставляет средства уров- ня платформы. Данное значение должно использоваться для пакета Microsoft.NE TCore . App Это значение указывает, что сборки в пакете применяются в процессе пос­ троения и не предоставляют какие-то средства , требующиеся приложению во время выполнения. Такое значение используется средством формиро­ вания шаблонов Visual Studio, конфигурирование которого рассматрива­ лось в главе 8
Глава 14 . Конфигурирование приложений 383 Регистрация инструментов разработки в файле project. json Определенные пакеты, добавляемые в раздел dependenc i es, будут предоставлять инструменты, которые применяются на стадии разработки и для своего функциони­ рования должны р егистрироваться в разделе too l s ф айла project . j s on . В листин­ ге 14 .6 показано добавление в раздел too ls новой записи, 1юторая регистрирует фун­ кциональность, пр едоставля емую пакетом Micr osoft . As p NetCore . Razor . Tool s; она добавляет поддержку IпtelliSense для встроенных дескрип торных вспомогатель­ ных классов к р едактору ф айл ов представле ний Razor среды Visual Studio. Листинг 14.6 . Регистрация инструментов в файле project. json "tools": { "Microsoft.AspNetCore.Razor.Tools": "1.0 .0-preview2-final", "Microsoft . AspNetCore . Server . IISinte g r a tion . Tools ": "l. 0 . 0- preview2 - final " }/ Когда вы добавляете инструменты в свой проект, пакеты обычно поступа­ ют с инструкциями о том, к ак должна выглядеть запись для ра здела tools файла project . j son. В листинге 14.6 можно видеть самые распространенные инструмен­ ты для дескрипторных вспо м огательных классов, присутствующие в проект ах, а так ­ же пакет, который добавляет команды Entity Framework Core для управления базами данных, как было описано в главе 8. Класс Program Класс Program определ е н в файл е по им е ни Program . cs и об е спечива ет точку входа для запуска прилож ения , предоставляя инфраструктуре .NET метод Main (). который может быть выполн ен для конфигурирования среды размещения и выбора класса , конфигурирующего приложение. В листинге 14.7 приведен стандар тный код кл ас са Program, добавляе мый к проектам средой Visual Studio. В больши нстве проектов изменять класс Program н е придется. е сли только вы не производите ра звертывание в необычной или высокоспециализированной среде разм е щения. Одно тако е измен е ние будет продемонстрировано в разделе "Работа со сложными конфигурациями" далее в главе , но в проектах, которые развертывают­ ся на стандартных платформ ах , подобных IIS или Azure , м ожно использовать класс Program, пр едлага емый по умолчанию . Листинг 14.7. Стандартное содержимое файла Program. cs using System . IO ; using Mic r osoft.AspNetCore.Hos t ing ; namespace ConfiguringApps puЬlic c l ass Prog ram { puЫic static void Main (string [ ] args) { var host = new WebHostBu i lder() .UseKestrel() . UseContentRoot(Direct ory. GetCu rrentDirectory()) . UseIISintegration()
384 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC .UseStartup<Startup>() . Build (); host.Run(); Первый оператор в методе Main () настраивает среду размещения, создавая объ­ ект WebHostBuilder и вызывая последовательность методов конфигурирования, ко­ торые описаны в табл. 14.6 . Таблица 14.6 . Методы конфигурирования в классе Program Имя UseKestrel () UseContentRoot() UseIISintegration() UseStartup () Build () Описание Этот метод конфигурирует веб-сервер Kestrel, как объясняет­ ся ниже во врезке "Использование Kestrel напрямую" Этот метод конфигурирует корневой каталог для приложения, который применяется для загрузки файлов конфигурации и доставки статического содержимого, такого как файлы изоб­ ражений, JavaScript и CSS Этот метод включает интеграцию с llS и llS Express Этот метод указывает класс, который будет использоваться для конфигурирования ASP.NET, как описано в разделе "Класс Startup" далее в главе Этот метод объединяет настройки конфигурации, предостав­ ленные всеми остальными методами, и подготавливает их к применению После того как конфигурация подготовлена, второе выражение в методе Main () запускает приложение, вызывая метод Run ( ) . В этот момент платформа размещения способна получать НТГР-запросы и направлять их приложению для обработки. Использование Kestrel напрямую При добавлении пакетов в файл proj ect. j son вы заметите, что одна из стандартных за­ писей в разделе dependencies (даже в проектах, созданных с использованием шаблона Empty) предназначена для Kestrel. "Microsoft.AspNetCore.Server.Kestrel": "1.0.О", Kestrel - это новый межплатформенный веб-сервер, спроектированный для выполнения приложений ASP.NET Core. Он задействуется автоматически, когда вы запускаете приложе­ ние ASP.NET Core с применением llS Express (сервер, предоставляемый средой Visual Studio для использования на стадии разработки) или полной версии llS, которая была традицион­ ной веб-платформой для приложений .NЕТ. При желании веб-сервер Kestrel также разрешено запускать напрямую, что означает воз­ можность выполнения приложений ASP.NET Core MVC на любой из поддерживаемых плат­ форм, обходя ограничение только операционной системой Windows, которое накладывает llS. Существуют два способа запуска приложения с применением Kestrel.
Глава 14. Конфигурирование приложений 385 Первый из них - щелчок на стрелке рядом с правой гранью кнопки llS Express в панели инс­ трументов Visual Studio и выбор элемента, который соответствует имени проекта. Это приведет к открытию нового окна командной строки и запуску приложения с использованием Kestrel. Того же результата можно достичь, открыв собственное окно командной строки, перейдя в папку с файлами конфигурации приложения (ту, которая содержит файл pro j ect. j son) и запустив следующую команду: dotnet run По умолчанию веб-сервер Kestrel начинает прослушивать порт 5000 на предмет поступле­ ния НТТР-запросов. Класс Startup Для конфигурирования функциональности приложений в ASP. NEТ Core применя­ ется класс С# по имени St ar tup . Он определен в файл е Startu p . cs , который Visual Studio добавляет в корневую папку проектов веб-приложений. Изучение работы клас ­ са Startup позволяет понять суть способа обработки НТТР-запросов и интеграции инфраструктуры MVC с остальными частями платформы ASP.NET. Совет. Имя класса Start u p предоставляется как параметр типа методу Us eSt art u p () , вызываемому в классе Pr ogram, т. е . при желании можно указать другое имя класса . В этом разделе мы начнем с простейшего из возможных классов Sta r tup и доба­ вим средства для демонстрации влияния различных конфигурационных параметров, получ ив в итоге конфигурацию, которая будет подходящей в большинстве проектов MVC. В кач естве отправной точки в листинге 14.8 по1шзан класс Star tup, добавля­ емый ср едой Visual Studio 1< прое1<там Empty, который настраивает всего лишь функ­ циональность, необходимую ASP.NET для обработки НТГР-запросов . Листинг 14.8. Начальное содержимое файла Startup.cs using Microsoft . AspNetCore . Bui l der ; u sing Microsoft.AspNetCore .H osting ; using Microsoft . As pNetCo r e .H ttp; using Mic r osoft . Exten sions . Depe nd encyinj ection ; usi ng Microsoft.Extensi ons .Logging; narn espace Con f ig uringAp p s { puЬlic clas s Star t up { puЫic void ConfigureServi ces(IServi ceCol lection servi ces) { } puЫic void Configure(IAppl icati onBuilder арр , I HostingEnvir onrne nt env , ILog g e r Factory l o g gerFactory) { loggerFactor y . AddConso l e() ; if (env . Is Deve loprn ent()) { app . UseDeve l operExce p t i onPage (); ap p. Run( async (con text ) => { await context .Response .WriteAsync ("H el lo World!") ; ));
386 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC В классе Startup определены два метода. ConfigureServices () и Configure (), которые настраивают совместно используемые средства, требующиеся приложению, и указывают ASP.NET о том, как они должны применяться. Работа этих методов объ­ ясняется в последующих разделах. Стандартный класс Startup содержит ровно столько функциональности, сколько нужно для ответа на НТГР-запросы с помощью простого сообщения, которое можно увидеть, запустив приложение (рис. 14.1). Рис. 14.1. Запуск приложения с исп ользованием стандартн о го класса Startup Особенности использования класса Startup Когда приложение запускается, инфраструктура ASP.NET создает новый экземп­ ляр класса Startup и вызьmает его метод ConfigureServices (), так что приложе­ ние может создать свои службы. Как объясняется в разделе "Службы ASP.NET" далее в главе, службы - это объекты, 1юторые предоставляют функциональность другим частям приложения. Приведенное описание не отличается особой строгостью по той причине, что службы могут применяться для предоставления практически любой функциональности. После того как службы созданы, ASP.NET вызывает метод Configure ().Целью ме­ тода Configure () является настройка конвейера запросов, который представляет со­ бой набор компонентов (называемых промежуточным программным обеспечением), используемых для обработки входящих НТГР-запросов и генерации ответов на них. В разделе "Промежуточное программное обеспечение ASP.NET" далее в главе будут даны объяснения, как работает конвейер запросов, и продемонстрировано создание компонентов промежуточного ПО. На рис. 14.2 показано, как ASP.NET имеет дело с классом Startup. Приложение за п ускается Создается Вызывается метод Вызывается экземпляр Configure метод кл асса Startup Services () Configure () Службы Конвейер созданы подготовлен Ри с. 14.2 . Применение класса Startup инфраструктурой ASP.NET для конфигурирования приложения Начинается обработка запросов Класс Startup, который для всех запросов просто возвращает одно и то же сооб­ щение "Hello World ! ",не особенно полезен, поэтому перед подробным обсуждением методов класса понадобится немного забежать вперед и включить МVС (листинг 14 .9).
Глава 14. Конфигурирование приложений 387 Листинг 14.9 . Включение MVC в файле Startup. cs using Mic r osoft.AspNetCore . Builder ; using Microsoft.AspNetCore .H ost ing; using Microsoft.AspNetCore.Http ; using Microsoft.Extensions . Dependencyinjection ; using Microsoft . Extensions . Logging ; namespace ConfiguringApps ( puЬlic class Startup { puЫic void ConfigureServices(IServiceCo llection services) ( services.AddМvc(); puЫic void Configure(IApplicationBuild er арр , IHostingEnvironrnent env , ILoggerFactory loggerFactory) ( app.UseМvcWithDefaultRoute(); Благодаря таким добавлениям (которы е объясняются в последующих разделах) инфраструктура может обрабатывать НТГР-запросы и генерировать ответы с исполь­ зованием контроллеров и представлений. Запустив приложение, вы увидите вывод , показанный на рис. 14.3 . х · --· ------· ------- ----------- , С 1 ф locatl1o st :SOOO ' (:; L___ ". ---".--"·-·- .. "_ __ ___" ____ --·-- -- .J - .,.. -·· ~. ··~ - 1 i\Ic.>s sage Пus is tl1e lпdex act io11 1 ! l______ - --- ,_______" ____ _ -- --· · ---------------- ____________.J Рис. 14.З. Результат включения MVC Обратите внимание, что содержимое не стилизовано. Минимальная конфигурация в листинг е 14. 9 не предоставляет какую-либо поддержку для обслуживания статичес­ кого содержимого, такого как таблицы стилей CSS и файлы JavaScript. Таким образом, элемент link в НТМL-разметке, визуализируемой представлением Index . cshtml, инициирует запрос для таблицы стилей CSS из Bootstrap, который приложение не в состоянии обработать, что предотвращает получение требуемой стилевой информа­ ции . Пробл ема будет устранена в раздел е '"Добавление оставшихся компонентов про­ межуточного программного обеспечения " далее в главе. Службы ASP.N ET Инфраструктура ASP.NEТ вызывает метод Startup . ConfigureServices (),т ак что приложени е может настроить требуемые службы. Термин "служба " относится к любому объекту, который предоставляет функциональность другим частям приложе­ ния. Как уже отмечалось, такое описание не является строгим , поскольку службы спо­ собны делать для приложения все что угодно. В качестве примера создайте в проекте папку Infrastructure и добавьте в нее файл класса по имени UptimeService. cs с опр еде лени ем, приведенн ым в листинге 14.10.
388 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 14.10. Содержимое файла UptimeService.cs из папки Infrastructure using System. Diagnostics; namespace Configuri ngApps . Infrastruct ure puЫic class UptimeService private Stopwatch timer ; puЬlic Upt i meServi ce() { timer = Stopwatch. Star t New() ; puЬlic l ong Upti me => t imer .ElapsedMi l l i seconds; Когда создается экземпляр класса Up timeServi ce , его конструнтор запускает тай­ мер, который отсл еживает, сколько вр емени выполнялось приложение . Это хороший пример службы, потому что класс UptimeService обе сп е чивает функциональность , которая мож ет применят ься в остальных частях прилож е ния и создается при з апуске приложения . Службы ASP.NET регистрируются с исполь зованием мет ода ConfigureService s () класса Startup; в листинг е 14 . 11 представлена регистр ация класс а UptimeService . Листинг 14.11. Регистрация специальной службы в фа йле Startup. cs using Mi crosoft . AspNetCore . Bui l der ; using Micr osof t .AspNetCore . Hosti ng ; u sing Microsoft. AspNetCore .H ttp ; u sing Micros oft. Ex t ens i ons . Dependencyin jection; us ing Mi c r osoft . Extension s . Logging ; using ConfiguringApps.Infrastructure; namespace Configuri ngApps puЬlic c l ass Startup { p u Ьl i c void ConfigureServices(ISe rviceCol l e c t i on services) { services.AddSingleton<UptimeService>(); servi ces.AddMvc() ; puЫic void Configure( I Ap p licat i o nBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { app .U seMvcWithDe f au l tRoute() ; Метод Confi gu r eSe r vices получает в качестве аргумен та объ е кт, который р е ­ ализу ет интерфейс IServiceCol l ect i on . Службы р е гистрир уются с помощью расширяющего метода, вызываемого на интерф ейсе IServiceCollection , кото­ рому указываются разнообразные конфигурационные параметры. Доступны е пара­ метры для создания служб будут описаны в главе 18, а пока мы применяем метод AddSingleton () , что означает совместное использование единственного объекта Up t i meServi ce по всему приложению.
Глава 14 . Конфигурирование прило же ний 389 Службы тесно связаны со средством под названием внедрение зависимостей, ко­ торое позволяет компонентам вроде контроллеров легко получать службы и подробно рассматривается в главе 18. Получить доступ к службам, зарегистрированным в мето­ де Startup . ConfigureServices (), можно за счет создания конструктора, который принимает аргумент с требуемым типом службы. В листинге 14.12 показан конструк­ тор, добавленный в контроллер Home для доступа к совместно используемому объекту UptimeService , которы й был создан в листинге 14. l l. Кроме того, обновлен метод действия Index () контроллера, так что он включает в генерируемые данные пред­ ставления значение свойства Update службы. Листинг 14. 12 . Доступ к службе в файле Hornecon troller. cs using System . Col l ections . Generic; using Microsoft.AspNetCore . Mvc ; using ConfiguringApps.Infrastructure; namespace ConfiguringApps . Controllers puЫic class HomeControl l er : Control l er private UptimeService uptime; puЬlic HomeController(UptimeService up) uptirne = up; puЫic ViewResult Index() => View(new Dictionary<string , string> { )); ["Message " ] = " This is the Index action ", [" Uptime " ] = $ " {uptime . Uptime)ms " Когда инфраструктуре MVC необходим экземпляр класса контроллера Ho me для обработки НТТР-запроса, она инспектирует конструктор HomeController и обнару­ живает, что ему требуется объект UptimeService . Затем MVC инспектирует набор служб , которые были сконфигурированы в классе Startup , выясняет, что служба UptimeService сконфигурирована так. чтобы для всех запросов применялся единс­ твенный объект UptimeService, и передает этот объект в качестве аргумента конс­ труктору при создании экземпляра HomeContro l ler . Службы могут регистрироваться и потребляться более сложными способами, но рассмотренный пример продемонстрировал главную идею , лежащую в основе служб, и показал, как определение службы в классе Star tup позволяет определять функци­ ональность или данные, к оторые используются по всему приложению. Запустив приложение и запросив стандартный URL, вы увидите ответ, который включает колич ес тво миллисекунд. прошедших с момента старта приложения. Это значение получа ется из объекта UptimeService, созданного в классе Sta r tup (рис. 14.4). Каждый раз, I<огда получается запрос к стандартному URL, инфраструктура MVC создает новый объе кт HomeControl le r и предоставляет ему разделяемый объект UptimeService в виде аргумента констру~пора. Это позволяет контроллеру Hom e по­ лучать доступ ко времени выполнения приложения. не заботясь о том, каким образом данная информация предоставлена или реализована.
390 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 1 ~ · С lф localhostSOCO --~: \ l ·- ·--·· --- ---==:=-::::: -:==-==== -- -:-= -=::::-::: -: - _____,___, Message П1is is the Iшlex actio11 ~~ ~~ 11 -- ------- ---··-"------··-· ------- ___ _J Рис. 14.4. Использование простой службы Службы MVC Пакет с таким уровнем сложности, как МVС , имеет дело со многими службами; одни из них предназначены для внутреннего употребления , а другие предлагают фун­ кциональность разработчикам. Пакеты определяют расширяющие методы, которы е настраивают все требующиеся им службы в единственном вызове метода. Для MVC такой метод имеет имя Ad dMvc ( ) и является одним из двух методов, добавленных в класс Sta r t up, чтобы обеспечить работу МVС: puЫic voi d ConfigureSe r vi ces (IServiceCollection ser vices) { ser vi ces .AddSi ngleton<UptimeSer vi ce>() ; services.Adc!Мvc(); Этот метод настраивает каждую службу, в которой нуждается MVC, не загромож­ дая метод Co nfigureServi ces () гигантским списком индивидуальных служб . На заметку! Средство lпtelliSeпse в Visual Studio будет отображать длинный список других расширяющих методов, которые можно вызывать на объекте реализации IServiceCol lection в методе Conf igureSe r vi ces () . Некоторые из этих мето­ дов, такие как AddS i ng leton () и Ad d Scoped () ,применяются для регистрации служб различными способами. Другие методы, подобные Ad dRouting () или AddCors () ,до­ бавляют отдельные службы, которые уже использовались методом AddМvc () . В резуль ­ тате для большинства приложений метод Confi gur e Servi ces () содержит небольшое число специальных служб, вызов метода Add Mv c ( ) и необязательно операторы конфи­ гурирования встроенных служб, которые описаны в разделе " Конфигурирование служб MVC" далее в главе. Промежуточное программное обеспечение ASP.N ET В ASP.NET Core термин промежуточное ПО используется в отношении компонен­ тов, которые объединяются для формирования конвейера запросов. Конвейер запро­ сов организован аналогично цепочке; когда поступает новый запрос, он п ередается первому компоненту промежуточного ПО в этой цепочке. Компонент инспектирует запрос и решает, обработать его и сгенерировать ответ или передать следующему ком­ поненту в цепочке. После того как запрос обработан, ответ, который будет возвращен клиенту, передается по цепочке обратно, что позволяет всем предшествующим ком­ понентам инспектировать или модифицировать его.
Глава 14. Конфигурирование приложений 391 Работ а компонентов пром ежуточного ПО может выгля деть несколько странной, но она допускает большую гибкость в том, как приложения собираются вместе. Понимание того, каким образом применение пром ежуточного ПО придает форму при­ л ож е нию , может оказаться важным, особенно если вы получаете не те ответы, кото­ рые ожидали. Чтобы выяснить, как функционирует система промежуточного ПО, мы создадим несколько специальных компонентов, которые продемонстрируют каждый из имеющихся четыр ех типов промежуточного ПО. Создание промежуточного по для генерации содержимого Самый важный тип промежуточного ПО генерирует содержимое для клиентов, и именно к этой категории принадлежит MVC. Чтобы создать компонент промежуточ­ ного ПО для генерации содержимого, не имея дела со сложностью MVC, добавьте в папку Infrastructure файл класса по им ени ContentMiddlewa r e . cs с определе­ ние м, прив еденным в листинге 14 . 13. Листинг 14.13. Содержимое файла Contentмiddleware.cs из папки Infrastructure using System . Text ; using System . Th r eading.Ta sks ; using Microsoft . AspNetCore . Htt p; namespace ConfiguringApps . Infra s tru cture puЫic class ContentMiddlewa r e { private Re q uestDe l egat e nex tDelegate; puЫic ContentMi dd l eware(Re q uestDe legate next) { nextDelegate = next ; puЬlic async Task I nvo ke(Http Con t ex t httpContext) if (httpContext . Re q uest . Path . ToStri ng () . ToLower () " /middl eware " ) await httpContext . Respon s e . WriteAs ync( " This i s from t he content mi ddleware", Encoding .UTFB) ; else { await n e xtDelegat e . Invoke (httpContext) ; Компоненты промежуточного ПО не реализуют какой-нибудь интерфейс и не яв­ л яются ун а сл едованными от ка~юго-то общего базового класс а. Взамен они определя­ ют конструктор, который принимает объект Re que s tDe l egate , и метод Invoke () . Объ ект RequestDelegate представляет следующий компонент промежуточного ПО в цепочке, а м етод Invoke ( ) вызывается, когда ASP.NET получ ае т НТГР -з апрос. Информация о запросе и ответе НТГР, которая будет возвращена клиенту, предо­ ставляется методу Invo ke () через аргумент HttpContext . Класс HttpContext и его свойства расс матриваются в главе l 7, а пока достаточно знать, что м етод I n vo ke ( ) в листинге 14.13 инспектирует НТГР-запрос и проверяет, был ли он послан на URL вида /middl eware . Есл и это так, тогда клиенту отправляется простой текстовый ответ; есл и использовался другой URL , то запрос перенаправля ется следующему компоненту в цепочке .
392 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Конвейер запросов настраивается внутри метода Configu r e () класса S tartup . В листинге 14 . 14 из прим е ра приложения удалены методы MVC и применен класс ContentMiddlewa r e в кач е стве единственного компонента в конвейере. Листинг 14.14. Использование специального компонента промежуточного ПО для генерации содержимого в файле Startup. cs using Mi crosoft.AspNet Core . Bui l der; using Mi crosoft.AspNetCore.Hosting; using Microso f t . AspN etCore . Http; using Micro s of t . Extensions.Dependen cyinjec t ion ; using Microsof t. Ex t ensions . Logging ; using ConfiguringApps. I nfrastructure ; namespace Confi gu r ingApps puЫic class Startup { pu Ы ic void ConfigureSe r v i ces (I ServiceCollection services) { se r vices . Ad d Singleton<Upt i meService>() ; puЫic void Configur e (IApplicati onBui l der арр, IHostingEnvir onment env, I Logger Factory l og g e rF actory) app.UseМiddleware<Contentмiddleware>( ) ; Специальные компоненты промежуточного ПО регистрируются с помощью расширяющего метода UseM i ddlewa r e () внутри метода Configure () . Метод UseMiddleware ( ) прим еня ет параметр типа для указания класса промежуточного ПО . Именно так инфраструктура ASP.NEТ Саге мож ет построить сп и сок всех компо­ нентов промежуточного ПО . которые собирается использовать, и затем создать их экз емпляры для формирования цепочки. Запустив приложение и запросив URL вида /middleware, вы увидит е р е зультат, п01tазанный на рис. 14 .5. На рис . 14 .6 изображен конвейер промежуточного ПО, созданный с применени­ ем класса ContentMiddl eware . Когда инфраструктура ASP.NEТ Core получает НТГР­ запрос, она передает его единств енному компоненту промежуточно г о ПО, зарегист­ риров анному в класс е Start up . Если URL является / midd l eware, т огда компонент генерирует результат, который возвращается ASP.NET Core и отправляется клиенту. Если URL отличается от /middleware , то класс Conte n tMiddleware п е редает запрос следующему компоненту в цепочке. Поскольку других компонентов нет. за­ прос достигает ограничительного обработчика, предоставленного инфраструнтурой ASP.NET Core при создании конвейера, который посылает запрос обратно через кон­ вейер в противоположном направл ении (после того. как вы озншшмитесь с работой других типов промежуточного ПО, данный процесс обретет больший смысл) . Использование служб в промежуточном ПО Применять службы , настроенные в методе Con f igureServices (), могут не толь­ ко контроллеры. Инфраструктура ASP.NET инспектиру ет конструкторы классов про­ межуточного ПО и использует службы для предоставления знач ений любым аргумен­ там , которые были определены .
Глава 14 . Конфигурирование прило ж ений 393 f:j l0<•1host 5000/middil!'Ф' Х ~ . с ~.:!!~~~~:::1~~.::._ _____ __3 ..1 ; 1 r-· -·· - -··-------- ------·-::;::1 ·- ·-- . --- -·- .. ___ __ _. ___ ~- -----··-"-··- -----· -- ---------- 1 Thi s is from the content middle1•1are / L__ ---- ---- ---- -- _ _______ J Рис. 14.5 . Генерация содержимого из специального компонента промежуточного ПО Промежуточное ПО для генерации содержимого 1- ш ' --- --- z r 1 а.: 1 Ответ 1 - 1 1 1 1 1 1 .D "ф ... s: ;r :s; :z; "'Q. ел ~ 1-----~ <( ' ~ о Рис . 14.6 . Пример конвейера промежуточного ПО В листинге 14 . 15 к конструктору класса ContentMiddleware добавлен ар­ гумент, который сообщает ASP.NET о необходимости предоставления объекта UptimeService . Листинг 14.15 . Применение службы в файле Contentмiddleware. cs using Systern . Text; using Systern . Threading .Tasks; using Microsoft.AspNetCore.Http; narnespace ConfiguringApps .I nfrastructure puЫic class ContentMiddleware { private RequestDelegate n ext Delegate; private UptimeService uptime; puЬlic Contentмiddleware(RequestDelegate next, UptimeService up) { nextDelegate = next ; uptime = up; puЫic async Task Invoke(HttpContext httpContext) if (httpContext . Request.Path.ToString() . ToLower() await httpContext.Response.WriteAsync( "This is from the content middleware "+ " /rniddleware " ) { $" (uptime: {uptime.Uptime}ms) ", Encoding .UTF8 ); else { awa it nextDelegate .Invoke(httpContext);
394 Часть 11. Подр о бные сведения об инфраструктуре ASP.NET Саге MVC Возможность использования служб означает, что компоненты промежуточного ПО могут разделять общую функциональность и избегать дублирования кода . Создание промежуточного по для обхода Следующий тип промежуточного ПО перехватывает запросы до того, как они до ­ стигнут компонентов для генерации содержимого, чтобы обойти проце сс прохожд е ­ ния конвейера , часто в целях , связанных с производительностью. В листинге 14 .16 приведено содержимое файла класса по имени ShortCircu i tMiddleware . c s, добав­ ленного в папку I nfrastructu re. Листинг 14. 16. Содержимое файла Sho rtCircui tмiddleware. cs из папки Infrastructure using System . Li nq; using Sy stem.Threadi ng .Ta sks ; usi ng Microsoft . AspNetCo r e . Http ; na me s pace ConfiguringApps . Inf r ast r uctu r e puЬlic c l ass ShortCircuitMiddleware { pri vate Reque s t Delegat e nextDeleg ate ; pu Ы ic Sho r tC i rcuitMiddleware(Req uestDelegate next) { nextDelega t e = next; puЫic async Ta s k Invoke(HttpContext httpContext) if (ht tpContext. Reque s t . Headers["User- Ag ent"J . Any(h => h.ToLower() .Contains("edge"))) { httpContex t . Response . Stat usCode = 403 ; else{ await nex t Delega te.I nvoke( h ttpContext) ; Этот компонент проме жуточного ПО проверяет заголовок User - Agent з апроса . который браузеры применяют для идентификации самих себя. Использование заго­ ловка Use r- Agent для опознания специфических браузеров не вполне надежно для реализации в реальном приложении, но достаточно для рассм атрива е мого прим ера . Понятие " обход" применяется из-за того, что такой тип промежуточного ПО не всегда направляет запросы следующему номпоненту в цепочке. В данном случае, если заголовок User - Agent содержит элемент edge, тогда компонент устанавливает код состояния в 403 - Forbidde n (403 - запрещено) и не направляет запрос следующе­ му компоненту. Так как запрос отклонен, нет никакого смысла давать возможность запросу обрабатываться остальными компонентами , что привело бы к ненужному потреблению системных ресурсов. Взамен обработка запроса прекращается раньш е, а клиенту посылается ответ 403. Компоненты промежуточного ПО получают запросы в поря дке. в котором они были настроены в классе S tartup , что означает необходимость настройки промежу­ точного ПО для обхода перед пром ежуточным ПО для генерации содержимого (лис­ тинг 14.17) .
Глава 14. Конфигурирование приложений 395 Листинг 14.17. Регистрация промежуточного ПО для о бх ода в фай л е S tartu p. cs using Microsoft . As p NetCore . Build er ; using Microso f t . AspNetCore . Hos ting ; using Mi crosoft . AspNetCore . Http ; using Microsoft . Extens i ons . Dep e ndencyinjection ; using Microsoft . Extens i ons.Logg i ng ; u s ing ConfiguringApps . Infrastructu re ; namespace Co n figuringApps puЬlic class Startup { p u Ьlic vo i d Confi gure Service s ( IServi ceCo l lect i on services) { services.AddSing l eton<Upt i meSe r vice>() ; puЫic void Configure(IAppl i cati onBuilder арр, I Host i ngEnvironme nt env, ILoggerFactory l oggerFacto r y ) { app . UseMiddleware<ShortCircuitмiddleware>() ; app.UseMiddleware<Cont entMi dd l ewa r e>() ; З апу с тив прило же ние и запросив любой URL с использованием брауз е ра Microsoft Edge, вы увидите ошибку 4 0 3. Запр осы из других браузеров компонент ShortCircui tMiddleware игно риру ет и п ередает следующему компоненту в цепоч­ ке , а это значит, что ответ будет сген ерирован , когда запрошенны м URL является /middleware . На рис . 14.7 иллюстрируется добавление в конвейер компонента про ­ межуточного ПО для обхода. 1- ш ~ ,.. z о.: (/) ~ <( ' Промежуточное ПО для обхода ------ 1 1 Ответ 1 1------ , ~ ,.. Промежуточное ПО для генерации содер жим ого ------- 1 1 Ответ 1 1------k .D <::; CJ) >-- :s: т s :r: "'Q_ 1о Рис. 14.7 . Добавление в конвейер компонента промежуточного ПО для обхода Создани е промежуточного ПО для редактирования запросов Следующий тип компонента промежуточного ПО, который мы опиш ем, не ген е­ рирует ответ. Вместо этог о они изменяет запр о сы перед тем, как они достигнут дру­ гих компон е нтов, н ах одя щихся дальше в це п очке . Такой вид промежуточного ПО при меня ется главным образом для интеграции с платформой, чтобы обогатить пр ед­ ставлени е ASP.NET Core запроса НТТР средствами, специфич ескими для платфор мы . О н также может ис пол ьзов ать ся для подготовки запросов, так что их легче обр аба-
396 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC тывать последующими компонентами. В качестве демонстрации добавьте в папку Infrastructure файл BrowserTypeMiddleware . cs с определением компонента пром ежуточного ПО, приведенным в листинге 14.18. Листинг 14.18. Содержимое файла BrowserТypeМiddleware. cs из папки Infrastructure us ing System . Linq ; us ing System . Th r eading . Tasks ; using Microsoft . AspNetCore.Http ; namespace ConfiguringApps . Infrastructure puЬlic class BrowserTypeMiddleware { private RequestDelegate nextDel e gate ; puЫic BrowserTypeMiddleware(RequestDelegate next) { nextDelegate = next; puЫic async Task Invoke(HttpContext httpContext) httpContext . Items[ " EdgeBrowser " ] = httpContext . Request . Headers[ " User - Agent " ] . Any(v => v . ToLower() . Contains( " edge " )) ; await nextDelegate.Invoke(httpContext) ; Новый компонент инспектирует заголовок User-Agent запроса и ищет в нем элемент edge , что предполагает возможность выдачи запроса браузером Edge. Объект HttpContext предоставляет через свойство Items словарь, который при­ меняется для передачи данных между компонентами, и ре зул ьтат поиска в заг о ­ ловке сохраняется с ключом EdgeBrowser. Чтобы продемонстрировать, как ком­ пон енты промежуточного ПО могут взаимодействовать, в листинге 14.19 показан класс ShortCircui tMiddleware, который отклоняет запросы, когда они поступа ­ ют от браузе ра Edge, принимая решение на основе сгенерированных компон ентом BrowserTypeMiddleware данных. Листинг 14.19. Взаимодействие с другим компонентом в файле ShortCircuitМiddleware.cs using System.Linq ; using System .Th reading.Tasks ; using Microsoft . AspNetCore .H ttp; namespace ConfiguringApps . Infrastructure puЬlic class ShortCircuitMiddleware { private RequestDelegate nextDelegate ; puЫic ShortCircuitMiddleware(RequestDelegate next) { nextDelegate = next ; puЬlic async Task Invoke(HttpContext httpContext) { if (httpContext. Items [ "EdgeBrowser"] аз bool? == true) httpContext . Response.StatusCode = 403 ;
Глава 14. Конф и гурирование прило ж ений 397 }else{ await nextDelegate .I nvoke(httpContext) ; И з-за своей природы компоненты промежуточного ПО , редакти рующие запро­ сы, необходимо помещать перед компонентами, с которыми они в заи модейству­ ют или которые полаг аются на вносимые ими измен ения. В листинг е 14.20 класс BrowserTypeMiddleware зар е гистрирован как пе рвый компонент в конв е йере. Листинг 14.20. Регистрация компонента промежуточного ПО для редактирования запросов в файле Startup. cs using Microsoft . AspNetCor e. Builder ; using Microsoft . AspNetCo r e . Hosting ; using Microsoft . AspNetCore . Http ; using Microsoft.Extens i ons . Dependencyln j ect i on ; using Microsoft . Extensions . Logging ; using ConfiguringApps.Infrastructure ; namespace ConfiguringApps { puЫic class Startup { puЫic void Conf i gureServices(IServiceCollection services) { services.AddSingleton<UptimeService>( ) ; puЫic void Configure( I ApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) app.UseМiddleware<BrowserTypeMiddleware>(); app . UseMidd l eware<ShortCircuitMi ddl eware>() ; app . UseMiddleware<ContentMiddleware>() ; Помещение компон ента в начале конвейера гарантирует, что до попадания в дру­ гие компоненты запрос уже будет модифицирован (рис . 14.8). ~ ш z а.: (f) <( ~ Проме жуточное ПО для редактирования запросов 1 Запрос 1 ~ Проме жуточное ПО для обхода -- - -- 1 1 Ответ 1 1----- r ~ Промежуточное ПО для генерации содер ж имого - --- --- 1 1 Ответ 1 1 -----f< Рис . 14.8. Добавление в конвейер компонента проме жуточного ПО для редактирования запросов .!J "' ф ~ 1~ I "'а. - _1о
398 Часть 11. Подробные сведения об и нфраструктуре ASP.NET Core MVC Создание промежуточноrо по для редактирования ответов Последний тип промежуточного ПО оперирует на ответах , генерируем ых другими компонентами в конвейере . Это полезно для регистрации в журнале деталей запросов и их ответов или для обработки ошибок. В листинге 14.21 представл ено содержимое файла Er r orMiddleware . cs, который добавлен в пап ку Infrastructure дл я де ­ монстрации данного вида ко мпон ента пром ежуточного ПО . Листинг 14.21. Содержимое файла ErrorMiddleware.cs из папки Infrastructure using Sys tern . Text ; using Systern . Threading . Tasks ; using Microsoft . AspNetCore . Http ; narnespace ConfiguringApps . Infrastructur e puЫic c l ass E rrorMidd l eware { private RequestDe l egate nextDe l egate ; puЫic ErrorMiddleware(Reques t De l egate next) { nextDe l egate = next ; puЫic async Task Invoke(HttpContext httpContext) await nextDelegate .I nvoke(httpContext) ; if (h t tpContext . Response . Statu s Code == 403) { await httpContext.Response . WriteAsync( " Edge not suppor t ed ", E ncod i ng . UTFB) ; else if (httpContext.Response.StatusCode == 404) { await httpContext . Response . WriteAsync( " No conte n t rniddleware response ", Encoding.UTF8) ; Компонент не заинтересован в запросе до тех пор, пока он не пройдет свой путь по конвей еру промежуточного ПО и не сгенерируется ответ. Если кодо м со стоя ния ответа является 403 или 404, тогда компонент добавляет к ответу описательное со ­ общение. Все другие ответы игнорируются . В листинге 14.22 показана р егистр ация кл асс а компон ент а в классе Startup. Совет. Вас может интересовать, от куда поступил код состояния 4 04 - Not Found (404 - не найдено) , пос кольку он не устанавливается ни одним из трех созданны х компонентов промежуточного ПО. Дело в том, что именно так инфраструктура ASP.NET конфигурирует ответ, когда запрос входит в конвейер, и этот ответ будет тем результатом, который воз­ вращается клиенту, если компоненты промежуточного ПО не изменяли его . Листинг 14.22. Регистрация компонента промежуточного ПО для редактирования ответов в файле Startup. cs using Mic r osoft . AspNetCore . Builder ; using Microsoft . AspNetCore . Hosting ; using Microsoft . AspNetCore . Http ;
Глава 14. Конфигурирование приложений 399 using Mi c r o soft . Extensions . Dependencyinj e ction ; using Microso ft . Extensions . Logging; using Co nfiguringApps . Infrastruct ure ; n am espace Co nfiguringApps { pu Ыi c class Startup { puЫic v o id ConfigureServices(IServiceCollection services) { services . AddSingleton<UptimeService>() ; puЫic void Configure(IApplicationBuilder арр , IHostingEnvironme n t e n v , ILoggerFactory l oggerFactory) app. UseMi ddleware<Erro rMiddleware>( ) ; app. UseMiddleware<BrowserTypeMiddleware>() ; app . UseMiddl eware<ShortCircuitMiddleware > () ; app. Use Middl eware<ContentMi ddle ware>( ) ; Класс Erro rMiddlewa r e зар е гистрирован в первой позиции конве й ера. Э т о м о­ жет выглядеть странным для компонента, который заинтересован только в ответах, но регистрация компонента в начале цепочки обеспечивает ему возможность инспек­ тировать ответы , сгенерированные любыми другими компонентами (рис. 14.9). В слу­ ч ае помещения ком понента дальше в конвейере он см ожет инсп ектировать отв еты , генерируем ые только ком понентами , находящимися после него . 1- ш~ z а.: ел~ <( Промежуточ ное П О для редактиров ан ия ответов ______ _____ ...._ ч Ответ ' П ромежуточ ное ПО дл я редактирован ия за п росов 1 Запрос 1 ' Промежуточное ПО б для о хода - - - -- 1 1 Ответ 1 1 - - - - - ' П ромежуточное ПО дл я генерации содержимо го - - - - - 1 1 Ответ 1 - 1--- - --~ Рис. 14.9 . Добавление в конвейер компонента промежуточного ПО для редактирования ответов Эфф ект от нового промежуточного ПО можно увидеть, запустив приложение и запрос ив любой URL кроме /middleware . Результато м будет сообщение об ошибке , показ анное на рис . 14.10. С) localhostSOOO/ otherURL Х No content middleware response Рис. 14.1О. Редактирование ответов от других компонентов промежуточного ПО
400 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Особенности вызова метода Confiqure () Инфраструктура ASP.NET Core исследует метод Configure () перед его вызовом и получает список его аргументов, которые он предоставляет с использованием служб, настроенных в методе ConfigureServices (), или специальных служб, описанных в табл. 14.7 . Таблица 14. 7. Специальные службы, доступные как аргументы метода Configure () Тип I App licationBuilder IHostingEnvir o nm ent ILoggerFactory Описание Этот интерфейс определяет функциональность, требуемую для настройки конвейера промежуточного ПО приложения Этот интерфейс определяет функциональность, требуемую для различения типов сред, таких как среда разработки и производственная среда Этот интерфейс определяет функциональность, требуемую для настройки регистрации в журнале запросов Использование интерфейса IAppl.ica tionВuil.der Хотя вы не обязаны определять любые аргументы для метода Conf i gure (), в большинстве классов Startup будет применяться , по крайней мере, интерфейс IApplicationBuilder, поскольку он позволяет создать конвейер пром еж уточно­ го ПО , как демонстрировалось ранее в главе . В случае специальных компонентов промежуточного ПО для регистрации классов используется расширяющий метод UseMiddleware ().Сложные пакеты промежуточного ПО для генерации содержимого предоставляют единственный метод, который настраивает все их компоненты пром е­ жуточного ПО за один шаг -точно так же, как они предоставляют единственный ме­ тод для определения применяемых ими служб. Для MVC доступны два расширяющих метода, описанные в табл. 14.8 . Таблица 14.8 . Расширяющие методы IApplicationBuilder для MVC Имя Описание UseMvcWi thDefa ultRoute () Этот метод настраивает компоненты промежуточного ПО для MVC с использованием стандартного маршрута UseMvc () Этот метод настраивает компоненты промежуточного ПО для MVC с применением специальной конфигурации мар­ шрутизации , указанной посредством лямбда-выражения Маршрутизация- это процесс, с помощью которого URL запросов сопоставля­ ются с контроллерами и действиями, определенными в приложении: маршрутиза­ ция подробно рассматривается в главах 15 и 16. Метод UseMvcWi thDefaul tRoute () полезен при изучении разработки приложений MVC, но в большинстве приложе­ ний вызывается метод UseMvc (), даже если результатом является явное опреде­ ление той же самой конфигурации маршрутизации, которая создается методом UseMvcWi thDefau l tRoute (), как показано в листинге 14.23. Такой подход делает конфигурацию маршрутизации , используемую в приложении, очевидной для других разработчиков и облегчает дальнейшее добавление новых маршрутов (что в какой-то момент требуется практически во всех приложениях).
Глава 14 . Конфигурирование прило ж ений 401 Листинг 14.23. Настройка компонентов промежуточного ПО для MVC в файле Startup. cs using Microsoft . AspNetCore . Bui l der ; using Microsoft . AspNetCore . Hosting ; using Microsoft . AspNetCo r e . Http ; using Microsoft . Extens i on s. Dependencyinj ection ; using Microsoft . Extensions. Logging ; using ConfiguringApps . Infr astructure ; narnespace Confi gur i ngApp s { puЫic class Startup { puЫic void ConfigureSe rvices(IServi ceCollection services) { services . AddSingleton<Upt i rneService>() ; services.AddМvc(); puЬlic void Configure(IAppl icationBu ilder арр , IHosti ngEnvironrnent env , ILog g e r Factory l oggerFactor y) app. Us eMiddlewa r e<Er r o rMiddl eware> ( ) ; app . UseMiddleware<Br ow serTypeMiddleware>() ; app . UseMiddleware<Shor t Ci rcui tM iddl eware>() ; app . UseMiddleware<Co n tentMiddl ew ar e>() ; app.UseМvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); Поскольку MVC настраивает компоненты промежуточного ПО для генерации со­ держимого, метод UseMvc () вызывается после регистрации всех других компонен­ тов промежуточного ПО. Чтобы подго товить службы , от которых зависит MVC, метод AddМvc () должен вызываться в методе Confi gureServices () . Использование интерфейса IHo s tingEnvironment Интерфейс IHost ingEnvirorune nt предоставляет базовую - но важную - инфор­ мацию о среде ра з м е щения. в которой функциониру ет прилож е ние, с применени е м свойств, описанных в табл. 14.9 . Свойства ContentRootPath и WebRootPath интересны, но в большинстве при­ ложений не нужны, потому что им еется встроенный компонент промежуточного ПО, которы й можно использовать для достав1ш статич еского содержимого , как описано в разделе " Включени е статического содержимого " дале е в главе. Важным свойством является Environme ntName , которо е позволя ет модифици­ ровать конфигурацию приложения на основе ср еды. где оно выполня ется. По согла­ шению существуют три распространенных среды (среда разработки (Deve l opment), подготовительная среда (Stagi ng) и производственная ср еда (Production)) . Текущая среда размещения устанавливается с применением пер ем енной среды по имени ASPNETCORE_E NVIRO NMENT . Чтобы установить переменную с реды, выбе­ рите пункт ConfiguringApps Options (Параметры Co n figur ingApps) из меню Project (Проект) в Visual Studio и перейдите на вкладку Debug (Отладка) . Дважды щелкни­ те на поле Value (Значени е) для пере м енной среды, по умолчанию установленной в Development, и и з м енит е ее на Staging (рис. 14. 11).
402 Ча сть 11 . Подробные сведен и я об инфрастру к туре ASP.NET Core MVC Таблица 14.9 . Свойства IHostingEnvironment Имя ApplicationName EnvironmentName ContentRootPath WebRootPath Описание Это сво йство возвращает имя приложения , установленное размещающей платформой Это свойство возвращает стро ку, которая описывает теку­ щую среду, как объясняется ниже Это свойство возвращает путь , по которому на ходятся файлы содер жимого и конфигурации пр и ложения Это свойство возвращает строку, указывающую каталог, в к отором находится статическое содер жимое для прило­ жения. Обычно это папка wwwroot ContentRootFileProvider Это свойство возвращает объе кт, который реализует ин ­ тepфeйc Mi crosoft . AspNetCore . FileProviders . IFileProvi der и может использоваться для чтения фай­ лов из папки, указанной свойством ContentRootPath WebRootFileProvider Это свойство возвращает объект, который реализует ин­ тepфeйc Microsoft . AspNetCore . FileProviders . IFileProvider и может применяться для чтения файлов из пап ки , указанной свойством WebRo o tPath А Profi!e Launch: 115 Eцi reS-' г:;. :.- :- -:- -:;:-;:--;:-::.;;- - - -- - L..... .-- - --· -·-· - Envisonment Venab~ s \/alue ASPN EТC OR E_E NoЛRONM E NT S:a~"9 \•/еЬ Str.-e r Settings "" ,.. .. ... ..-- ". ",__ ,,j..... " _____",·-"-~,,,,..,...... " . Р ис. 14 .11. Установка имени среды раз м ещения Сохраните изменения, чтобы новое имя среды вступило в силу. Совет. Имена сред не чувствительны к регистру символов , так что Staging и staging трактуются ка к одна и та же среда . Хотя Development, Staging и Production явля­ ются традиционными именами сред , вы можете использовать любое желаемое имя. Это может быть полезно, напри м ер , когда над проектом трудятся несколько разработчи ков , каждый из которы х требует разных конфигурационных настрое к. В разделе " Работа со сло жными конфигурациями " далее в главе объясняется, ка к учитывать сложные различия ме жду конфигурациями сред.
Глава 14 . Конфигурирование приложений 403 Внутри метода Configure () можно выяснить, какая среда размещения приме­ няется, прочитав свойство IHostingEnvironrnent. En vironrnentName или исполь­ зуя один из расширяющих методов , которые оперируют над объектами реализации IHostingEnvironrnent и описаны в табл. 14.10. Таблица 14.10. Расширяющие методы IHostingEnvironment Имя Описание IsDeveloprnent () Этот метод возвращает true, если именем среды размещения является Developrnent IsStaging () IsProduction() IsEnvironrnent(env) Этот метод возвращает true, если именем среды размещения является Staging Этот метод возвращает true , если именем среды размещения является Production Этот метод возвращает true, если имя среды размещения сов ­ падает со значением аргумента env Расширяющие методы применяются для изменения набора компонентов про­ межуточного ПО в конвейер е, чтобы приспособить поведение приложения к раз­ личным средам размещения. В листинге 14.24 с помощью одного из расширяю­ щих методов гарантируется, что специальные компоненты промежуточного ПО, созданные ранее в главе, присутствуют только в среде размещения Developrnent. Листинг 14.24 . Использование среды размещения в файле Startup . cs using Microsoft . AspNetCore .B uilder ; using Microsoft . AspNetCore .H osting ; using Microsoft . AspNetCore . Http; using Microsoft . Extensions . Dependencyi nje ction ; using Microsoft . Extensions . Logging ; using ConfiguringApps . Inf r astructure ; namespace ConfiguringApps { puЫic class Start u p { puЫic void ConfigureServices(IServic eColl ection services) { services . AddSingleton<UptimeService>(); services.AddMvc() ; puЬlic void Configure( IAppli cati onBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseМiddleware<ErrorMiddleware>(); app.UseМiddleware<BrowserTypeМiddleware>(); app.UseМiddleware<ShortCircuitмiddleware>(); app.UseМiddleware<Contentмiddleware>(); app.UseMvc(routes => routes.MapRoute( name : " default ", template : " {cont rol ler=Home}/{action=Index}/ {id?} "); });
404 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Специальные компоненты промежуточного ПО не добавляются в конвейер при текущей конфигурации, в которой переменная среда была установлена в Staging. Запустив приложение и запросив URL вида /middleware. вы получите ошибку 404 - Not Found, т.к. доступными компонентами промежуточного ПО являются лишь те, которые настроены методом UseMvc (), а они не имеют контроллера для обработки указанного URL. На заметку! Протестировав влияние от изменения среды размещения, не забудьте изменить ее снова на Development, иначе примеры в оставши хся главах книги не будут работать должным образом . Использование интерфейса ILoggerFactory Интерфейс ILoggerFactory используется для конфигурирования регистрации в журнале внутри приложения, так что отдельные компоненты могут предоставлять диагностическую информацию. Метод Configure () класса Startup применяется для указания, куда отправляется журнальная информация, и какова требуется сте­ пень детализации . Чтобы предоставить доступ к функциональности регистрации в журнале, добавьте в проект новый пакет, как показано в листинге 14 .25. Листинг 14.25. Добавление пакета для регистрации в журнале в файле project. json " dependencies ": )' " Microsoft.NETCore.App ": "version": "1.0 .0", " type ": "platform" )' " Microsoft.AspNetCore.Diagnostics" : " 1 . 0 . О ", " Microsoft.AspNetCore . Server .IISintegration ": "1. 0 .0 ", " Microsoft.AspNetCore . Server . Kestrel ": " 1.0.0", " Mi crosoft.Extensions . Logging . Console ": "1.0 .0", "Microsoft.Extensions.Logging.Debug" : 11 1.0.0 11 , " Microsoft. AspNetCore. StaticFi les": "1. О. О ", " Microsoft . AspNetCore. Mvc ": "1. О. О ", " Mi cros oft . VisualStudio . Web . Browser Link . Loader": "1 4.0 . 0" , " Microsoft . AspNetCore . Razor .T ools ": ( " version ": " l . 0 . 0-preview2-final" , "type ": "bui ld " В листинге 14 .26 приведена базовая конфигурация регистрации в журнале, кото­ рая подходит для проектов на стадии разработки. (В рассматриваемом примере не задействована точная настройка, обеспечиваемая внешним файлом конфигурации, которая демонстрируется позже в главе.)
Глава 14 . Конфигурирование прило ж ений 405 Листинг 14.26. Конфигурирование регистрации в файле startup. cs using Microsoft.AspNetCore . Builder ; using Microsoft . AspNetCore .H osting; using Microsoft .AspNetCore.Http; using Microsoft . Extensions . Dependencyinjection ; using Microsoft.Extensions . Logging ; using ConfiguringApps . Infrastructure ; namespace ConfiguringApps puЬlic class Startup { puЬlic void ConfigureServices(IServi ceCollection services) { services.AddSingleton<UptimeService>(); services . AddMvc(); puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment env, ILoggerFactory loggerFactory) loggerFactory.AddConsole(LogLevel.Debug); loggerFactory.AddDebug(LogLevel.Debug); if (env.IsDevelopment()) { app .U seMiddleware<ErrorMiddleware>() ; app.UseMiddleware<BrowserTypeMiddleware>() ; app .U seMiddleware<ShortCircuitMiddleware>() ; app .UseMiddleware<ContentMiddleware>(); app . UseMvc(routes => routes . MapRoute( name : " default ", template : "{ controller=Home}/{action=Index}/{id?} " ) ; }); Аргумент типа ILoggerFactory метода Conf igure () предоставляет объект, необ­ ходимый для конфигурирования системы регистрации в журнале. гл авная задача при настройке регистрации заключается в указании, куда планируется отправлять жур­ нальные сообщения, для чего предназначены методы AddConsole () и AddDebug () . Метод AddConsole () посылает журнальные сообщения на консоль, что удобно в слу­ чае запуска приложения из командной строки с использованием Kestrel, как было описано ранее в главе. Метод AddDebug ( ) отправляет журнальные сообщения в окно Output (Вывод) среды Visual Studio, когда приложение выполняется под управлением отладчика. Эти два оператора полезны для получения регистрационной информации во время разработки. Совет. Посылать сообщения отладки можно не только на консоль и в окно Output. Доступны также варианты с применением журнала событий и пакетов регистрации от независи­ мых разработчиков, таких как Nlog . Для этого понадобится добавить в приложение па­ кет Microsoft . Extensions . Logging. EventLog или Microsoft. Extensions . Logging . NLog и вызвать метод AddEventLog () или AddNLog () соответственно на объекте реализации ILoggerFactory внутри метода Configure ().
406 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Система регистрации в журнале ASP.NET Core определяет шесть уровней отладоч­ ной информации, которые описаны в табл. 14 . 11 в порядке своей важности. Таблица 14.11 . Уровни отладочной информации ASP.NET Core Уровень Trace Debug Information Warning Error Critical None Описание Этот уровень используется для сообщений, которые полезны на стадии разработки, но не требуются в производственной среде Этот уровень применяется для детализированны х сообщени й, необхо­ димых разработчикам при отладке в случае возни кновения проблем Этот уровень ис пользуется для сообщений , которые описывают общее функционирование приложения Этот уровень применяется для сообщений, описывающи х события, кото­ рые являются непредвиденными , но не прерывают работу приложения Этот уровень используется для сообщений , которые описывают ошибки , прерывающие работу приложения Этот уровень применяется для сообщений, которые описывают катаст­ рофические отказы Этот уровень используется для от ключения регистрационны х сообщен ий Чтобы включить все сообщения, методам AddConsole () и AddDebug () в качес ­ тве аргумента п ередается значение LogLevel . Debug . Оценить эффект от включения регистрации в журнале можно, запустив приложение с применением отладчика Visual Studio и заглянув в окно Output, где появятся р егистр ационные сообщения, которые описывают, как обрабатывается каждый НТТР-запрос, например: Microsoft.AspNetCore.Hosting.Internal . WebHost : Inforrnation: Request starting НТТР/1 . 1 GET http://l ocalhost :SOOO/ Microsoft . AspNetCore . Routing . RouteBase : Debug: Request successfully matched the route with name 'default ' and template ' {controller=Home}/ {action=Index}/{id?} '. Microsoft.AspNetCore.Mvc.Internal . ControllerActioninvoker:Debug : Executing action ConfiguringApps . Control l ers . HomeController . Index (ConfiguringApps) Microsoft . AspNetCore . Mvc . Internal . ControllerActioninvoker : Inforrnation : Executi ng action rnethod ConfiguringApps . Controlle r s . HomeController . Index (ConfiguringApps) with arg uments () - Mode lState is Valid Microsoft.AspNetCore . Mvc. In ternal .C ont r ollerActioninvoker : Debug : Executed action method ConfiguringApps . Controllers.HomeController . Index (ConfiguringApps) , returned result Microsoft . AspNetCore . Mvc . ViewResult . Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine:Debug: View lookup cache hit for view ' Index ' in controller ' Ноте' . Microsoft.AspNetCore . Mvc . ViewFeatures.Internal. ViewResultExec utor:Debug : The view ' Index ' was found . Microsoft.AspNetCore . Mvc . ViewFeatures.Internal .
Глава 14 . Конфигурирование прило ж ений 407 ViewResultExecutor : Information : Executing ViewResult , running view at path /Views/Home/Index . cshtml. Microsoft . AspNetCore . Mvc .Inte r nal . ControllerActioninvoker : Information : Е xecuted action ConfiguringApps.Controllers . HomeController . Index (ConfiguringApps) in 8 . 9685ms Microsoft . AspNetCore . Hosting . Inte r nal.WebHost : Information : Request finished in 16 . BBбms 200 text/html ; c harset= u tf - 8 Microsoft . AspNetCore . Serve r. Kestrel : Debug : Connection id " OHKTSDOEU8D4U " completed keep alive response . Microsoft . AspNetCore . Hosting . Internal . WebHost : Information : Request starting НТТ Р/1 . 1 GET http : //localhost : SOOO/liЬ/bootstrap/dist/css/bootstrap - theme . min . css Microsoft . AspNetCore . Builder.Route r Mi ddleware : Debug : Request did not match any routes . Microsoft . Asp NetCore . Host ing.Interna l. WebHost : Information: Req u est starting НТТР/1 . 1 GET http : //localhost : SOOO/l i Ь/bootstrap/dist/css/bootstrap . min . css Microsoft . AspNetCore . Hosting . Interna l. WebHost : Information : Request fin i shed in 48 . 4602ms 404 Microsoft . AspNetCore . Builder . Route r Middleware : Debug : Request did not match any routes . Microsoft . AspNetCore . Server . Kestrel : Debug : Connection id " 0HKT5DOEU8D4V " completed keep al i ve r esponse . Microsoft . AspNetCore . Hosting . Inte r na l. WebHost : I n format i on : Request finished in 85 . 0848ms 404 Microsoft . AspNetCore.Server . Kestrel : Debug : Connection id " OHKT5DOEU8D50 " comp l eted k e ep alive r esponse . Создание специальных журнальных записей Хотя журнальные записи, создаваемые встроенны ми компонентами ASP.NET Core и МVС , очень пол езны, вы можете пр едоставить более точные сведения о том, как ра­ ботает приложение , создав ая собственные журн ал ьные записи . Запись журнального сообщения осуществляется в два этапа: получение объекта регистратора в журнале и записывание сообщения. В листинге 14 .27 приведен контроллер Home с подде ржкой р егистрации в журнал е . Листинг 14.27. Создание специальных журнальных записей в файле HomeController.cs using System . Collections . Gener i c ; using Microsoft.AspNetCore . Mvc ; using ConfiguringApps . Infrastructure ; using Microsoft.Extensions.Logging; namespace ConfiguringApps . Controllers puЫic class HomeController : Contro l ler private UptimeService uptime ; private ILogger<HomeController> logger; puЫic HomeController(UptimeService up, ILogger<HomeController> log) { uptime up ; logger = log;
408 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC puЬlic ViewResul t Index () { logger.LogDeЬug($"Handled {Request.Path} at uptime {uptime.Uptime}"); return View(new Dictionary<string, string> ["Message " ] = " This is the Index act ion", [ " Uptirne "] = $ " {uptirne . Uptirne}rns " }); Интерфейс ILogger определяет функциональность, требуемую для создания жур­ нальных записей и получения объекта, который реализует этот интерфейс, а класс HorneController имеет аргумент конструктора с типом ILogger<HorneController> . Параметр типа позволяет системе регистрации в журнале использовать имя класса в журнальных сообщениях, причем значение для аргумента конструктора предостав­ ляется автоматически через средство внедрения зависимостей, которое рассматри­ вается в главе 18 . Имея объект реализации ILogger, можно создавать журнальные сообщения с при­ менением расширяющих методов, определенных в пространстве имен Microsoft. Extensions . L ogging. Методы предусмотрены для всех уровней регистрации, пере­ численных в табл. 14.11. Класс HomeController использует метод LogDebug () для создания сообщения на уровне Debug. Чтобы увидеть результат, запустите приложе­ ние с применением отладчика Visual Studio и поищите в окне Output журнальное со­ общение вроде следующего: ConfiguringApps.Controllers.HorneController : Debug: Handled / at uptirne 19326 Когда приложение запус1<ается, в окне Output появляется много сообщений , что может затруднить восприятие индивидуальных сообщений. Одиночные сообщения легче различать, если щелкнуть на кнопке Clear All (Очистить все) в верхней части окна Output и затем перезагрузить страницу в браузере - это гарантирует отображе­ ние только журнальных сообщений, которые относятся к отдельному запросу. Добавление оставшихся компонентов промежуточного программного обеспечения Существует набор наиболее часто используемых компонентов промежуточного ПО, которые полезны в большинстве проектов MVC и применяются в примерах, рас­ сматриваемых в настоящей книге. В последующих разделах по мере добавления этих компонентов в конвейер запросов будет объясняться их работа. Обеспечение возможности обработки исключений Даже самое тщательно написанное приложение будет сталкиваться с исключения­ ми, по этому важно их должным образом обрабатывать. В листинге 14.28 демонстри­ руется добавление в конвейер запросов компонентов промежуточного ПО, которые имеют дело с исключениями.
Глава 14 . Конфигурирование приложений 409 Листинг 14.28. Добавление компонентов промежуточного ПО для обработки исключений в файле s tartup. cs using Microsoft . AspNetCore.Builder ; using Microsoft . AspNetCore . Hosting ; using Microsoft . AspNetCore . Http ; using Microsof t.Extensions.Dependencyinjection; using Microsoft .E xtens i ons. L ogging ; using Configuri ngApps . Infrastructure ; namespace Config uringApps puЫic class Startup { puЫic void Conf i gureServices(IServiceCo ll ect i on services) { services.AddSingleton<UptimeServi ce>() ; services . AddMvc() ; puЬlic void Configure(IApplicationBui lder арр , IHostingEnvironment env , ILoggerFactory loggerFactory ) loggerFactory.AddConsole(LogLeve l . Debug); logge r Factory . AddDebug(LogLevel.Debug); if (env . IsDeve l opment()) { app . UseDeveloperExceptionPage(); app.UseStatusCodePages(); else { app . UseExceptionHandler("/Home/Error"); app . UseMvc(rou tes = > routes . MapR oute( name : " default ", temp l ate: " {co ntroller=Home} /{act i on=Index } / {id?}"); }); Метод UseStatusCodePages () добавляет к ответам , не имеющим содержимого, описательные сообщения, такие как 404 - Not Found (404 - не найдено) , которое мо­ жет быть удобным. по скольку не все браузеры отображают пользователю собственные сообщения. Метод UseDeveloperExceptionPage () настраивает компонент промежуточного ПО для обработки ошибок, который отображает детали исключения в ответе, в том числе связанную трассировочную информацию. Такая информация не должна быть видимой поль з ователям. поэтому вызов UseDeve loperExcep tionPag e () делается только в среде разработки, которая опр еделяется с помощью объекта реализации IHostingEn vi ronnunent. В подготовительной или производств е нной среде взамен используется метод UseExcept i onH andler () .Он настраив ает обработку ошибок, позволяющую отобра­ жать специальное сообщение об ошибке , которое не раскрывает особенности внут­ рен ней работы приложения. Аргументом метода UseExceptionHandler () является URL. на который должен быть перенаправлен клиент, чтобы получить сообщение об
41 О Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC ошибке . Это может быть любой URL. предоставляемый приложением, но по соглаше­ нию применяется / Home/Error . В листинге 14.29 добавлена возможность генерации исключ ений по требованию действия Index контроллера Home и определено действие Error, таr< что запросы, генерируемые UseExceptionHandler () ,могут быть обработаны . Листинг 14.29. Генерация и обработка исключений в файле HomeController. cs using Sys t em.Collections . Generic ; using Microsoft.AspNetCore . Mvc ; using ConfiguringApps . Infrastructure ; using Microsoft.Extensions.Logging; namespace ConfiguringApps . Controllers pub'lic class HomeController : Controller private UptimeService uptime ; private ILogger<HomeController> logger ; puЫic HomeController(UptimeService up , ILogger<HomeController> log) uptime up; logger log ; puЬlic ViewResult Index(bool throwException = false) if (throwException) { throw new System.NullReferenceException(); logger . LogDebug($ " Handled {Reque st.Path} at up t ime {uptime.Uptime} " ) ; ret urn View (new Dictionary<string , string> ["Message"] = "T his is the Index action ", [ " Uptime " ] = $ " {upt ime.Uptime}ms" }); puЬlic ViewResult Error() return View("Index", new Dictionary<string, string> { [ "Message"] = "This is the Error action" }); Внесенные в действие Index изменения с помощью средства привязки моделей, которое обсуждается в главе 26, обеспечивают получение значения throwException из запроса. Действие генерирует исключение NullReferenceException, если значение throwException равно true, и выполня ется нормально, если значение throwException равно fa l se . Действие Error использует представл е ние Index для отображения про­ стого сообщения. Запустив приложение и запросив URL вида /Home/Index? throwException= true , можно просмотреть результаты функционирования разных компонентов промежуточного ПО для обработки исключений. Строка запроса пре­ доставляет значение для аргумента действия Index, а наблюдаемый ответ будет
Глава 14. Конфигурирование приложений 411 зависеть от имени среды размещения. На рис. 14.12 показан вывод, порождаемый методом UseDeveloperExceptionPage () (для среды размещения Development) и методом UseExceptionHandler () (для всех остальных сред размещения). Q rr.tc~ ! S1t1W1frl'Q1' ~ (- с [р~~!~;!5~~~-;~;~~~~~~.;:~~--~:=-==-~-=~:~::-----------·--1 . ~ An unha nd led exceptioп occшrecl whil e pюcessing t/1e r D """' ' ----·---- ___________ -, 1 . • . , . 1 • J~·, С 1(!) l~fllho$t ;5000/Нo in•/1Mi'"? t hrc.\\(11.Cf"P !>Ol'l - lrut '(( 1 f ~ f\:ul iRerereгceExcep:юn: O~ect reference not Set to an instance 01 an сЬ;есч .. - - ~~--.:;---:=.::.--,:_-..::-"::-~:---:-:;:-::--·- - --- -· :.:::.=-:-.·:.;с;-:_,_···-::~.;:-.:' - r ! Cci nfi9ur1 r9.Gpps.U)ntro!lf!'S Homl!-ContФ\ler!nde:ir(8ooJtJn 1rtrowE~ci-pt1cn) 1n ~~Controlltr -' ~· 11\ :-irм~~I" Thic is tlie Bn-oi· nctio" '.' ' ШJ О""'У Cook1e> Headois I___ -- -" -· -·· -- ]-~~--~~~_____J ; Nt1HRefe reoceExcep~ion : Object refe r·erкe nGt set ю an юstance of an object. Сс1' ~·_, 1:-~Лщ~: :;1;;r, · r. • :1c:rs HJ1·~eec"·шc:~cr:r.cг~ ВО:.~;.>.:'"1t'"''·)". ~ \ (€;)'.{"Jl'\J1n lk>ir>eControll~r . cs ; _,J..'·~~~~$·~~s,~~tpti7~t~)~#.,,.. Рис. 14. 12 . Обработка исключений в среде разработки и подготовительной/производственной среде Страница исключения для разработчика предлагает детали исключения. а также возможность исследовать его информацию трассировки стека и запрос, который при­ вел к исключению. В противоположность этому страница исключения для пользова­ теля должна применяться просто для указания, что что-то пошло не так. Включение средства Browser Link Средство Browser Link (Ссьmка на браузер) было описано в главе 6 и предназна­ чено для управления браузерами на стадии разработки. Серверная часть средства Browser Link реализована как компонент промежуточного ПО, который должен быть добавлен к классу Startup в качестве части 1юнфигурации приложения, потому что без нее интеграция с Visual Studio работать не будет. Средство Browser Link полезно только на стадии разработки и не должно использоваться в подготовительной или производственной среде. поскольку оно модифицирует ответы, генерируемые дру­ гими компонентами промежуточного ПО, с целью вставки кода JavaScript. который открывает Н1ТР-подключения с серверной стороной, чтобы она могла получать уве­ домления о перезагрузке страницы. В листинге 14.30 показано, как вызывать метод UseBrowserLink (), который регистрирует компонент промежуточного ПО только для среды размещения Development. Листинг 14.30. Включение средства Browser Link в файле Startup. cs using Microsoft . AspNetCore.Builder; using Microsoft.AspNetCore .H osting; using Mic rosoft.AspNetCore.Http; using Microsoft .E xtensions . Dependencyinjection; using Microsoft.Extensions.Logging; using ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЬlic cla ss Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc();
412 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { loggerFactory.AddConsole(LogLeve l.Debug) ; loggerFactory . AddDebug(LogLevel . Debug) ; if (env . Isoevelopment()) ( app.UseDeveloperExceptionPage() ; app . UseStatusCodePages() ; app.UseBrowserLink(); else { app . UseExceptionHandler ( " /Home/Error " ) ; app . OseMvc(routes => ro ut es.MapRoute( пате : " default ", template: " {cont r olle r=H ome}/{action = Index}/{id?} " ) ; )); Включение статического содержимого Финальный компонент пром ежуточного ПО, полезный для большинства проектов, предоставляет доступ к файлам в папке wwwroot , так что приложения могут включать изображения, файлы JavaScript и таблицы стилей CSS. Метод UseSt ati cFiles () добавляет компонент, который обходит конвейер запросов для статических файлов (листинг 14.31). Листинг 14.31. Включение статического содержимого в файле Startup. cs using Microsoft . AspNetCore . Bui l der ; using Microsoft.AspNetCo re.Hosting; using Microsoft . AspNetCore.Http ; usin g Microsoft.Extensions . Dependencyin je ction ; usi ng Microsoft . Exte n sions .Logging ; using Configuri ngApps .In frastructure ; namespace Conf iguringApps puЫic class Startup { puЫic void Configu r eServices(IServiceCol l ection services) { services . AddSingleton<Opt imeService>() ; services . AddMvc() ; puЬlic void Configure(IApplicationBuilder арр , IHostingEnvironment епv , ILoggerFactory loggerFactory) loggerFactory.AddConsole(LogLevel.Debug); loggerFa ctory .AddDebug(LogLevel . Debug) ; if (env .IsDeve l opment()) ( app .U seDeveloperEx cept i onPage() ; app . UseStatusCod e Pages() ; app . UseBrow serLink();
Глава 14 . Конфигурирование прило ж ений 41 З else{ app .UseExceptionHandler( "/ Home /Er ror " ) ; app.UseStaticFiles(); app . UseMvc(routes => { routes.MapRoute( name : "defaul t ", template : " {cont r oller =Home }/{acti o n=Index}/{ i d?} " ) ; }); Статическое содержимое обычно требуется вне зависимости от среды размеще­ ния, из-за чего метод UseStaticFi l es () вызывается для всех сред. Это означает, что элемент link в представлении I n d ex будет надлежаще работать и позволит бра­ узеру загруз ить таблицу стилей CSS из Bootstrap. Запустив прилож е ние, можно уви­ деть результат (рис . 14 . 13) . С) Re~vlt Message Uptime х Th is is the lndex action 28087ms '----- - -------- --· -------- ------- ' Рис. 14.13. Включение статического содержимого Использование данных конфигурации Конфигурацию в классе Sta r tup можно дополнять внеrшшми конфигурационными данными , которые позволяют конфигурации изменяться без необходимости в модифи­ кации кода внутри класса Star t u p . Соглашение предусматривает применение конструк­ тора класса Startup для загрузки конфигурационных данных, по этому они могут быть досrупны в методах ConfigureServices () и Con f igure () ,когда они вы зываются. Конфигурационные данные могут храниться в файлах JSON или ХМL, читаться из командной строки или пр едоставляться через переменные среды. Формат JSON явля­ ется предпочтительным для новых проектов ASP.NET Core, а соглашени е заключается в том, чтобы начать с файла по имени a pps ett ings . j son. В целях демонстрации добавьте в проект файл appsett ing s. j s o n с использованием шаблона элемента ASP.NET Configuration File (Файл конфигурации ASP.NET) и поместите в него настрой­ ки, приведенные в листинге 14 .32. Совет. Соглашение предполагает добавление конфигурационных данных в файл appsetting s. j s o n , но вы можете также создавать и пользоватьсs~ новыми файлами конфигурации, если располагаете достаточным объемом данных длs~ того, чтобы их хра­ нение в одном файле затрудняло управление ими.
414 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 14.32. Содержимое файла appsettings. j son " Logging ": { " IncludeScopes ": false, "L ogLevel ": { " Default": "Debug", "S ystem ": " Information ", " Microsoft ": " Information " }' " ShortCircuitMiddl eware" : " EnaЬleBrowserShortCircuit ": true Конфигурационные данные ASP.NET Core состоят из пар "ключ-значение" , кото­ рые могут группироваться в разделы . В файле appsettings. j son, показанном в листинге 14.32, им еетс я раздел Logging, который содержит одну пару "ключ-зн аче ­ ние" (IncludeScopes - ключ. false - значение) и один раздел LogLevel . В свою очередь раздел LogLeve l содержит три пары "ключ-значение" с ключами Defaul t. System и Microsoft. Есть таюке раздел под названием ShortCircuitMiddleware, который содержит единственный ключ EnaЫeBrowserShortCircui t. Чтение конфигурационных данных Инфраструктура ASP.NET Core предоставляет набор пакетов NuGet, которые при­ меняются для чтения конфитурационных данных из разнообразных источников и описаны в табл. 14.12. Каждый пакет предлагает расширяющий метод, предн азна­ ченный для чтения конфигурационных данных . Таблица 14.12. Пакеты NuGet для чтения конфигурационных данных Имя Microsoft.Extensions. Configuration Microsoft . Extensions . Configuration.Json Micro soft . Extensions . Configuration . CommandLine Microsoft.Extensions. Configurat i on. EnvironmentVariaЬles Microsoft . Extensions . Configuration.Ini Microsoft . Extensions . Configuration.Xml Описание Этот пакет предоставляет основную поддержку конфигура­ ционных данных и может использоваться для определения настроек программным образом с применением метода AddinMemoryCollection() Этот пакет используется для чтения конфигурацион­ ных данных из файлов JSON с применением метода AddJsonFile () Этот пакет используется для чтения конфигурационных данных из командной строки с применением метода AddCommandLine ( ) Этот пакет используется для чтения конфигурационных данных из переменных среды с применением метода AddEnvironmentVariaЫes() Этот пакет используется для чтения конфигурационных дан­ ных из файлов INI с применением метода AddiniFile () Этот пакет используется для чтения конфигурационных дан­ ных из файлов ХМL с применением метода AddXmlFile ()
Глава 14 . К онфигурирование прило ж ений 415 Чтобы загрузить конфигурационны е данные из файл а appsetting . j son, не об­ ходимо добавить в файл proj ec t . j s on основной пакет для чтения конфигурацион ­ ных данных и пакет JSON , как показано в листинге 14.33 . Листинг 14.33 . Добавление пакетов в файле package. j son " dependencies " : }' " Microsoft . NETCore . App 11 : "version": "1.0.О", " type ": 11 platform " }1 " Microsoft . AspNetCore . Diagnostics ": "1.0.0", " Microsoft . AspNetCore . Server . IISintegration 11 : "1.0.О", " Microsoft . AspNetCore . Server . Kestrel ": "1. О . О ", " Microsoft . Extensions . Loggi ng . Console ": " 1 . О. О ", " Micr6soft . Extensions . Logg i ng . Debug 11 : 11 1.0 .0 11 , " Microsoft . AspNetCore . StaticFiles ": 11 1.О.О", " Microsoft . AspNetCore . Mvc ": 11 1.О.О 11 , " Microsoft . VisualS t udio . Web . BrowserLink . Loader ": " 14 . 0 . 0 11 , " Microsoft . AspNetCore . Razor.Tools ": }' 11 version": "l .0 .0-preview2-final11 1 " type ": "build" "Мicrosoft.Extensions.Configuration 11 : 11 1.0.0 11 , "Microsoft.Extensions.Configuration . Json 11 : 11 1.0.0 11 Чтобы прочитать содержимое файла appse ttings . j son , в класс Startu p добав­ л е н кон структор , в которо м с помощью м етода AddJsonFi l e () осуществля ется чте­ ние файла appset tings . j son (листинг 14.34). Листинг 14.34. Чтение конфигурационных данных в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . AspNetCore .H osting ; using Microsoft . AspNetCore . Http ; using Microsoft . Extensions . Dependencyinjection ; using Microsoft . Extensions . Logging ; using ConfiguringApps . Infrastructure ; using Microsoft.Extensions.Configuration; namespace ConfiguringApps puЫic cla ss Startup { puЬlic Startup(IHostingEnvironment env) Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .A dd JsonFile( 11 appsettings.json 11 ) . Build(); puЬlic IConfigurationRoot Configuration { get; set; }
416 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC puЫic void ConfigureServices(IServiceCollection services) services.AddSingleton<UptimeService>(); services.AddMvc(); } puЬlic void Configure(IApplicationBui l der арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { 11 .. . для краткости остальные операторы не показаны . .. Целью конструктора является установка значения свойства Configuration, воз­ вращающего объект, который реализует интерфейс IConfigurationRoot, обеспе­ чивающий доступ к конфигурационным данным. Интерфейс IConfigurationRoot представляет точку входа в конфигурационные данные и является производны м от интерфейса IConfiguration, который также используется для представления инди­ видуальных разделов конфигурации. Конструктор Startup может принимать специальные службы, описанны е в табл. 14.7. Служба IHostingEnvironrnent требуется, когда конфигурационные дан­ ные загружаются, потому что свойство ContentRootPath пр едоставля ет доступ к ка­ талогу, который содержит файл appsettings . j son. Процесс загрузки конфигурационных данных состоит из трех шагов. Первый шаг заключается в создании нового объекта ConfigurationBuilder. Второй шаг предус­ матривает загрузку данных из индивидуальных источников с применением рас ши­ ряющих методов, таких как AddJsonFile (). Третий шаг предполагает вызов метода Build () на объекте ConfigurationBuilder, который соз дает структуру пар "'клю ч­ значение" и разделы , после чего присваивает результат свойству Configuration. Доступно несколько версий метода AddJsonFile (), описанных в табл. 14.13. Вьпuе использовалась простейшая версия метода AddJsonFile (), которая сгенерирует ис­ ключение, если файл не существует, и будет игнорировать любые изменения в файл е . Повторная загрузка конфигурационных данных Система конфигурации ASP.NET Core подцерживает повторную загруз ку данных , когда фай­ лы конфигурации изменяются . Некоторые встроенные компоненты промежуточного ПО , та­ кие как система регистрации в журнале, подцерживают эту функциональность, что означает возможность изменения уровней регистрации во время выполнения без необходимости в перезапуске приложения. Аналогичные средства можно также предусмотреть при разработ­ ке специальных компонентов промежуточного ПО. Однако просто тот факт, что средство делает что-то возможным, вовсе не означает, что оно является целесообразным . Внесение изменений в файлы конфигурации производственных систем - верный способ вызвать простой из-за отказа . Слишком легко допустить ошибку при модификации и получить неправильно работающую конфигурацию . Даже при успешном изменении могут возникать непредвиденные последствия, такие как переполнение дисков регистрационными данными или резкое снижение производительности . Рекомендуется избегать динамического редактирования и удостоверяться, что все измене ­ ния прошли через стандартные процедуры разработки и тестирования в подготовительной среде, прежде чем развертывать их в производственной среде. Может возникнуть соблазн заняться модификацией активной системы, чтобы установить причину проблемы, но это редко заканчивается хорошо. Если вы обнаруживаете, что занимаетесь редактированием файлов конфигурации производственной системы, то должны задаться вопросом, готовы ли вы превратить небольшую проблему в гораздо более крупную.
Глава 14 . Конфигурирование прило ж ений 417 Таблица 14.1 З. Вер сии метода AddJsonFile () Метод AddJsonFile(name , optional, reload) AddJsonFile(name , optional) AddJsonFile(name) Описание Этот метод загружает данные из указанного файла. Если файл не существует и значение аргумента optional равно false , тог­ да генерируется исключение . Если значение аргумента reload равно true, то конфигурационные данные будут обновляться в случае изменения файла JSON Эквивалентно вызову AddJsonFile (name , optional , false) Эквивалентно вызову AddJsonFile (name , false , false) Работа с данными конфигурации Данные из файла appsettings . j son доступны через свой ств о Configuration, добавленное в класс Startup , которо е во зв раща ет объект, реализующий интерфейс IConfigurationRoot. Доступ к значениям данных производится посредством ком­ бинации членов , определенных упомянутым интерфейсом, и расширяющих методов из табл. 14 .14 . Таблица 14.14. Члены и расширяющие методы для интерфейса IConfigurationRoot Имя [key ] GetSection(name) GetChildren () GetReloadToken () Reload () Описание Этот индексатор применяется для получения строкового значения, соответствующего указанному ключу key Этот метод возвращает объект реализации IConfiguration , который представляет раздел конфигурационных данных Этот метод возвращает перечисление объектов реализации IConfiguration, которые представляют подразделы те- кущего объекта конфигурации Этот метод возвращает объект реализации IChangeToken, который может использоваться для получения уведомления , когда имеется изменение в конфигурационных данны х Этот метод принудительно перезагружает конфигурацион­ ные данные GetConnectionString (name) Этот метод эквивалентен вызову GetSection ( " ConnectionStrings") [name] Что бы получить значение, понадобится пройти по структуре данных до требу­ ем ого раздела конфигура ции, представленного с помощью объекта , который реа­ лизует интерфейс IConfigura tion , предоставляющий подмножество доступных для IConfigurationRoot членов (табл. 14.15). В листинге 14.35 осущ ест вля етс я перемещение по данным для нахождения раздела конфигурации ShortCircuitMiddleнare и получ ения значения настройки EnaЫeBrowserShortCircui t, чтобы выяснить, добавлять ли специальные компо­ ненты пром ежуточно го ПО в конвейер запросов.
418 Часть 11. Подробные сведения об инфр а структуре ASP. NET Core MVC Таблица 14.15. Члены, определенные интерфейсом IConfiguration Имя [key] GetSection(name) GetChildren () Описание Этот индексатор применяется для получения строкового значения , соответствующего указанному ключу key Этот метод возвращает объект реализации IConfiguration , который представляет раздел конфигурационных данных Этот метод возвращает перечисление объектов реализации IConfiguration, которые представляют подразделы текущего объекта конфигурации Листинг 14.35. Использование данных конфигурации в файле Startup. cs puЫic void Configure(IApplicationBuilder арр , IHostingEnvironment env , ILoggerFactory loggerFactory) { if (Configuration.GetSection("ShortCircuitмiddleware 11 ) ? [ 11 EnaЫeBrowserShortCircuit 11 ] == 11 True") { app.UseMiddleware<BrowserTypeМiddleware>(); app.UseMiddleware<ShortCircuitмiddleware>(); loggerFactory.AddConsole(LogLevel.Debug); loggerFactory.AddDebug(LogLevel . Debug); if (env . IsDevelopment()) { app.OseDeveloperExceptionPage(); app . OseStatusCodePages() ; app . OseBrowserLink() ; else { app . OseExceptionHandler( " /Home/Error " ) ; app . OseStaticFiles() ; app . OseMvc(routes = > { routes.MapRoute( name : 11 default 11 , template : 11 {controller=Home}/{action=Index}/{id?} " ); )); Использование данных конфигурации для встроенных компонентов промежуточного программного обеспечения Некоторые компоненты промежуточного ПО могут настраиваться с прим енением конфигурационных данных. Самым распространенным примером является пакет ре­ гистрации в журнале, который позволяет предоставлять уровни регистрации для раз ­ ных компонентов через раздел конфигурации. Раздел logging. включенный в файл appsettings . j son в листинге 14.32, может использоваться с расширяющими мето­ дами, которые настраивают получат елей журнальных сообщений (листинг 14.36) .
Глава 14. Конфигурирование прило же ний 419 Листинг 14.36 . Применение конфигурационных данных для настройки регистрации в журнале в файле Startup. cs loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(LogLevel.Debug); Meтoд AddConsole () перегружендляприемаобъектареализации IConfiguration, который конфигурирует. какой вывод должен быть послан на консоль. Раздел Logging /LogLevel файла appsettings . j son используется для фильтрации жур ­ нальных сообщений , отправленных на консоль. Например, в листинге 14.37 показано . как можно фильтровать сообщения, зарегистрированные классом HorneCont r o ller, так что будут отображаться только сообщения уровня Cri tical . На заметку! Такая фильтрация вступает в силу, только когда приложение запускается из ко­ мандной стро ки с применением Kestrel напрямую, как описано во врезке "И спользование Kestrel напрямую" ранее в главе. Листинг 14.37. Фильтрация журнальных сообщений, выводимых на консоль, в файле appsettings. j son "Logging": { "I ncludeScopes ": fa lse, "L ogLevel ": { "D efault ": " Debug", " System": "Information", " Microsoft": "Information", "ConfiguringApps.Controllers.HomeController": "Critical" }' " ShortCircuitMiddleware": " EnaЫeBrowserShortCircuit ": true Конфигурирование служб MVC Когда метод AddMvc () вызывается внутри метода ConfigureServices (). он на­ страивает все службы, которые требуются приложениям МVС. Преимущество такого подхода заключается в удобстве. поскольку все службы МVС регистрируются за один шаг, но если необходимо изменить стандартное поведение, то понадобится выполнить дополнительную работу по реконфигурированию служб. Метод AddMvc () возвращает объект, реализующий интерфейс IMvcBuilder, а инфраструктура MVC предлагает набор расширяющих методов, предназначенных для расширенного конфигурирования, наиболее полезные из которых описаны в табл. 14.16. Многие методы конфигурирования связаны со средствами, рассматрива­ емыми в последующих главах.
420 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 14.16. Полезные расширяющие методы IMvcBuilder Имя AddМvcOptions () AddForrnatterMappings() AddJsonOptions( ) AddRazorOptions() AddViewOptions () Описание Этот метод конфигурирует службы, используемые инф­ раструктурой MVC, как объясняется ниже Этот метод применяется для конфигурирования средс­ тва, которое позволяет клиентам указывать формат по­ лучаемых данных (глава 20) Этот метод используется для конфигурирования способа создания данных JSON (глава 20) Этот метод применяется для конфигурирования меха­ низма визуализации Razor (глава 21) Этот метод используется для конфигурирования способа обработки представлений инфраструктурой MVC , в том числе применяемые механизмы визуализации (глава 21) Метод AddMvcOptions () конфигурирует самые важные службы MVC. Он прини ­ мает функцию , получающую объект MvcOptions, который предоставля ет набор кон­ фигурационных свойств; наиболее полезные свойства перечислены в табл. 14 .17 . Таблица 14.17. Избранные свойства MvcOptions Имя Описание Conventions Это свойство возвращает список соглашений модели, ко­ торые используются для настройки способа создания конт­ роллеров и действий инфраструктурой MVC (глава 31) Filters Это свойство возвращает список глобальных фильтров (глава 19) ForrnatterMappings Это свойство возвращает сопоставления, применяемые для того, чтобы позволить клиентам указывать формат по­ лучаемых ими данных (глава 20) InputForrnatters Это свойство возвращает список объектов, используемых для разбора данных запросов (глава 20) ModelBinders Это свойство возвращает список связывателей модели, которые применяются для разбора запросов (глава 26) ModelValidatorProviders Это свойство возвращает список объектов , используемых для проверки достоверности данных (глава 27) OutputForrnat ters Это свойство возвращает список классов, которые формати­ руют данные, отправляемые из контроллеров АР! (глава 20) RespectBrowserAcceptHeader Это свойство указывает, должен ли учитываться заголовок Accept , к огда принимается решение о том , как о й формат данных задействовать для ответа (глава 20) Описанные в табл. 14.17 методы конфигурирования используются для точ­ ной настройки способа функционирования инфраструктуры МVС, и в указанных главах вы найдете подробны е описания связанных с ними средств. Тем не менее.
Глава 14. Конфигурирование приложений 421 в качестве небольшой демонстрации в листинге 14.38 показано, как можно приме­ нять метод AddМvcOptions () для изменения параметра конфигурации. Листинг 14.38. Изменение параметра конфигурации в файле Startup. cs puЫic void ConfigureServices(IServiceCollection services) { services .AddSingleton<UptimeSe r vice>() ; services .AddМvc() .AddМvcOptions(options => { options.RespectBrowserAcceptHeader = true; }); Лямбда-выражение, передаваемое методу AddMvcOptions () , принимает объект MvcOptions , который используется для установки свойства RespectBrowserAccept Header в true . Это изменение позволяет клиентам оказывать большее влияние на формат данных. выбираемый процессом согласования содержимого, как объясняется в главе 20. Работа со сложными конфигурациями Если вы нуждаетесь в поддержке большого количества сред размещения или меж­ ду средами размещения имеется много отличий, тогда применение операторов if для ветвления конфигураций в классе Startup может в результате дать конфигура­ цию , которую трудно читать и редактировать, не вызывая неожиданных изменений . В последующих разделах будут описаны разнообразные способы использования клас­ са Startup для сложных конфигураций. Создание разных внешних конфигурационных файлов Когда вы загружаете конфигурационные данные из внешнего источника, такого как файл JSON, настройки и значения конфигурации переопределяют любые сущес­ твующие данные с теми же самыми именами. Это означает, что вы объединяете мно­ гочисленные файлы с целью переопределения частей конфигурационных данных для разных ср ед размещения . Например, в листинге 14.39 приведено содержимое фай­ ла по имени appsettings . development. j son, который был создан с применением шаблона элемента ASP.NET Configuration File . Конфигурационные данные в этом файле устанавливают значение EnaЬleBrowserShortCircui t в false . Совет. Может nоказаться, что файл appsettings . development . j son исчезает сразу же nос­ ле его создания. Щелкнув на стрелке слева от заnиси appsettings . j son в окне Solution Explorer, вы увидите, что Visual Studio груnnирует вместе элементы с nохожими именами. Листинг 14.39. Содержимое файла appsettings. development. j son " ShortCircuitMiddleware ": { " EnaЫ e BrowserShortCircuit ": false
422 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Чтобы загрузить эти данные, необходимо добавить в конструктор Startup новый вызов метода AddJsonFile (),включив в имя файла название среды размещения и удостоверившись в установке аргумента optional в true , так что в случае недоступ­ ности конфигурационного файла, специфичного для среды. исключение возникать не будет. Требуемые изменения приведены в листинге 14.40. Листинг 14.40. Загрузка конфигурационного файла, специфичного для среды, в файле Startup. cs puЬlic Startup(IHostingEnvironment env) { Configuration = new ConfigurationBuilder() .SetBas ePath(env . ContentRootPath) . A dd JsonFile ( "a ppsettings .json " ) .AddJsonFile($ 11 appsettings.{env.EnvironmentName}.json11 , true) . Build() ; Конфигурационные файлы загружаются в порядке их указания, поэтому настройки в более поздних файлах переопределяют настройки в файлах, указанных раньше. В ре­ зультате значением EnaЬleBrowserShortCircui t будет false, когда приложение находится в среде Development, и true - когда в среде Staging и Production. Внимание! При развертывании приложения вы обязаны обеспечить включение дополнитель­ ных конфигурационных файлов. В главе 12 рассматривался пример включения конфигура­ ционного файла во время развертывания в Azure. Создание разных методов конфигурирования Выбор разных файлов с конфигурационными данными может быть удобен, но не предоставлять полного решения для сложных конфигураций, т.к. файлы данных не содержат операторы С#. Если вы хотите варьировать операторы конфигурирования, используемые для создания служб или регистрации компонентов промежуточного ПО. тогда можете применять разные методы, причем имя метода должно включать название среды размещения (листинг 14.41). Листинг 14.41. Использование разных имен методов в файле Startup. cs using Microsoft . AspNetCore.Builder; using Microsoft.AspNetCore . Hosting; using Microsoft . AspNetCore . Http; using Microsoft . Extensions.Dependencyinjecti on ; using Microsoft . Extensions .Loggi ng; us ing ConfiguringApps . Infrastructure; using Microsoft.Extensions . Configuration ; namespace ConfiguringApps puЫic cl ass Startup { puЫic Startup(IHostingEnvironment env) { Configuration = new ConfigurationBuilder()
Глава 14 . Конфигурирование прило ж ений 423 . SetBasePath(env . ContentRootPath) . A ddJsonFile( " appsettings . json " ) . Ad dJsonFile($ " appsettings . {env .E nvironmentName} .j son" , true) . Build(); puЫic IConfigurationRoot Configuration { get ; set ; } puЫic void ConfigureServices(IServiceCollection services) services . AddSingleton<UptimeService>() ; services.AddMvc() . AddMvcOptions(options => { options . RespectBrowserAcceptHeader = true ; }); puЫic void ConfigureDevelopmentServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddМvc(); puЬlic void Configure(IApplicationBuilder арр) { app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); puЫic void ConfigureDevelopment(IApplicationBuilder арр, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(LogLevel . Debug); app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseBrowserLink(); app.UseStaticFiles(); арр. UseMvc (routes = > { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); Когда инфраструктура ASP.NET Core ищет методы ConfigureService () и Configure () в классе Startup, она сначала выясняет, имеются ли методы, имена которых включают название среды размещения. В листинге 14.41 был добавлен ме­ тод ConfigureDevelopmentServices (), который в среде Development будет при­ меняться вместо метода ConfigureServices (),и метод ConfigureDevelopment (), который в среде Development будет использоваться вместо метода Configure () .Вы можете определить отдельные методы для каждой среды, которую необходимо подде­ рживать, и полагаться на стандартные методы. вызываемые в случае н едосту пно сти
424 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC методов, специфичных для той или иной среды. В рассматриваемом примере это оз­ начает, что методы Configur eServi ces () и Configure () будут применяться для подготовительной и производственной сред. Внимание! Стандартные методы не вызываются, если определены методы, специфич­ ные для среды . Например, в листинге 14.41 инфраструктура ASP.NET Саге не будет вызывать метод Configure () в среде Developrnent, потому что имеется метод ConfigureDevelopment (). Другими словами, каждый метод несет ответственность за полную конфигурацию, требуемую для среды, на которую он ориентирован. Создание разных классов конфигурирования Использование разных методов означает, что вы не обязаны применять операторы if для проверки имени среды размещения, но в результате могут быть получены крупные классы, которые сами по себе явлmотся проблемой. В случае особенно сложных конфи­ гураций последним движением вперед будет создание собственного конфигурационного класса для каждой среды размещения . Когда инфраструктура ASP.NEТ Core ищет класс Startup, она первым делом проверяет. есть ли класс с именем, включающим назва­ ние текущей среды размещения. С этой целью в про ект добавлен файл класса по имени StartupDevelopment . cs , содержимое которого показано в листинге 14.42 . Листинг 14.42 . Содержимое файла StartupDevelopment. cs using ConfiguringApps .I nfrastructure ; using Microsoft . AspNetCore . Builder ; using Microsoft . AspNetCore .H osting ; using Microsoft .E xtensions . Configurat i on ; using Microsoft.Extensions.Dependencylnjection; us ing Microsoft.Extensions.Logging; namespace ConfiguringApps { puЫic class StartupDevel opment puЫ i c StartupDevelopment (IHost ingEnvironment env) { Configuration = new ConfigurationBuilder() .SetBasePath(env . ContentRootPath) .AddJsonFile( " appsettings .json ") . AddJsonFile($ " appsettings . {env . EnvironmentName} . json ", true) . Build() ; puЫic IConfig u rationRoot Configuration { get ; set ; } puЬlic void ConfigureServices(IServiceCollection services) services . AddSingleton<UptimeService>() ; services . AddMvc() ; puЫic void Configure(IApplicationB u ilder арр , I LoggerFacto r y loggerFactory) loggerFactory . AddConsole(Conf i gu r ation.GetSecti on( "L ogging " )); loggerFactory . AddDebug(LogLevel . Debug) ; app . UseDeveloperExceptionPage() ; app . UseStatusCode Pages() ; app.UseBrowserLink(); app . UseStaticFiles() ;
app .U seMvc(rou t e s => routes. MapRout e ( narne : "defaul t ", Глава 14 . Конфи гурирование при л о ж ений 425 ternpl ate : " {control ler=Horne }/ {action=Index}/{i d?}") ; }); Класс Star tupDevelopment содержит методы ConfigureServi c e s () и Confi gu re () , которые специфичны для среды размещения De v elopment. Чтобы предоставить инфраструктуре ASP.NEТ Core возможность найти класс St a rtup , спе­ цифичный для среды , в класс Prog r am потребуется внести изменение, показанное в листинге 14 .43 . Листинг 14.43 . Включение класса Startup, специфичного для среды, в файле Program . cs using Systern . I O; us ing Mi crosoft .AspN etCore.Hosting ; namespac e Config u ringApps puЫic c l ass Pr ograrn { puЫic st atic void Main(string[] args) { var host = new WebHostBuilder () . UseKe strel() . UseConten tRoot( Directo r y . Ge t CurrentDi re c t ory()) . UseIISint egration () . UseStartup( " Co nfiguringApps") . Build(); host.Run() ; Вместо указания специфичного класса методу Us e Star t u p () передается имя сборки, которая должна использоваться. Когда приложение запускается, инфраструк­ тура ASP. NEТ Core будет искать класс, имя которого включает названи е среды разме­ щения, такой как Star tupDevelopme nt или Startu pP r odu ction, и возвращаться к применению класса Startup, если специфичный для среды класс отсутствует. Резюме В настояще й главе рассматривались способы конфигурирования приложений MVC. Были описаны конфигурационные файлы J SON, объяснены особенности использо­ вания класса St artu p и дано введение в службы и промежуточное ПО. Вы узнали, как обрабатыв аются запросы с применением конвейера. и каким образом различные типы промежуточного ПО используются для управления потоком запросов и ответов. В следующей главе будет представлена система маршрутизации, посредством кото­ рой инфраструктура MVC имеет дело с отображением URL запросов на контроллеры и действия .
ГЛАВА 15 Маршрутизация URL в ранних версиях ASP.NET предполагалось наличие прямой связи между запра­ шиваемыми URL и файлами на жестком диске сервера. Работа сервера заключа­ лась в получении запроса от браузера и доставке вывода из соответствующего файла. Подобный подход успешно работал для инфраструктуры Web Forms, в которой каждая страница ASPX представляла собой и файл, и самодостаточный ответ на запрос . Это совершенно не имеет смысла в приложении MVC, где запросы обрабатывают­ ся методами действий из классов контроллеров, и однозначное соответствие между запросами и файлами на диске отсутствует. Для обработки URL в MVC платформа ASP.NET применяет систему маршрутиза­ ции, которая была модернизирована для инфраструктуры ASP.NEТ Core. В настоящей главе вы узнаете, как использовать систему маршрутизации для обеспечения мощной и гибкой обработки URL в своих проектах . Вы увидите, что система маршрутизации позволяет создавать любой желаемый шаблон URL и выражать его в ясной и лаконич­ ной манере. Система маршрутизации выполняет две функции. • Исследование входящих URL и выбор контроллера и действия для обработки запроса. • Генерация исходящих URL. Это URL, которые появляются в НТМL-разметке, ви­ зуализированной из представлений, так что специфическое действие может быть инициировано, когда пользователь щелкает на ссылке (после чего ссылка снова становится входящим URL) . В главе внимание будет сосредоточено на определении маршрутов и их примене­ нии для обработки входящих URL, чтобы пользователь моr достичь контроллеров и действий. Есть два способа создания маршрутов в приложении МVС: маршрутизация на основе соглашений и маршрутизация с помощью атрибутов . Здесь мы рассмот­ рим оба подхода. Затем в следующей главе будет показано, как использовать те же самые маршру­ ты для генерации исходящих URL, которые необходимо включать в представления, а также объяснены способы настройки системы маршрутизации и продемонстрирова­ но применение связанного средства под названием области. В табл. 15.1 приведена сводка, позволяющая поместить маршрутизацию в контекст.
Глава 15 . Маршрутизация URL 427 Таблица 15.1 . Помещение маршрутизации в контекст Вопрос Что это такое? Чем она полезна? Как она используется? Существуют ли какие­ то с к рытые ловушки или ограничения? Существуют ли альтернативы? Изменилась ли она по сравнению с версией MVC 5? Ответ Система маршрутизации несет ответственность за обработку вхо­ дящих запросов, а также выбор контроллеров и методов действий для их обработки . Вдобавок система маршрутизации используется для генерации маршрутов в представлениях, известных как исхо­ дящие URL Система маршрутизации обеспечивает гибкую обработку запросов без привязки URL к структуре классов в проекте Visual Studio Сопоставление URL с контроллерами и методами действий опре­ деляется в файле Startup. cs или путем применения атрибута Route к контроллерам Конфигурация маршрутизации для сложного приложения может стать трудной в управлении Нет. Система маршрутизации - это неотъемлемая часть ASP.NET Core Система маршрутизации работает в значительной степени тем же самым образом, как в предшествующих версиях инфраструктуры, но с изменениями, которые отражают более тесную интеграцию с плат­ формой ASP.NET Core . Маршруты , основанные на соглашениях, определяются в файле Startup . cs , а не в теперь уже устаревшем файле RouteC on f ig . cs . Классы маршрутизации теперь определены в пространстве имен Mi c r osoft . AspNetCo re. Rout ing . Маршруты больше не соответствуют URL, если нет подходящего контроллера и метода действия в приложении (в предыдущих вер­ сиях MVC маршруты могли соответствовать URL, но по-прежнему возвращали ошибку 404 - Not Found (404 - не найдено)). Основанные на соглашениях стандартные значения, необязательные сегменты и ограничения маршрутов теперь могут быть выра жены как часть шаблона URL, используя тот же синтаксис, что и при мар­ шрутизации с помощью атрибутов. Запросы к статическим файлам (таким как изображения, CSS и JavaScript) теперь обрабатываются выделенным промежуточным ПО (как было описано в главе 14) . Наконец, изменение способа, которым реализована система марш­ рутизации, затрудняет модульное тестирование маршрутов. Такая тенденция появилась с введением маршрутизации на основе атри­ бутов в MVC 5, но изолировать систему маршрутизации для модуль­ ного тестирования больше невозможно В табл. 15.2 представлена сводка для настоящей главы.
428 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 15.2 . Сводка по главе Задача Решение Отображение между URL и методами Определите маршрут действий Возможность не указывать сегменты URL Сопоставление сегментов URL, ко­ торые не имеют соответствующих переменных маршрутизации Передача сегментов URL методам действий Возможность не указывать сегменты URL, для которых не предусмотрены стандартные значения Определите стандартные значения для сегментов маршрута Определите статические сегменты Определите специальные перемен­ ные сегментов Определите необязательные сегменты Листинг 15.1-15.10 15.11 -15. 13 15.14-15.17 15.18-15.20 15 .21-15.22 Определение маршрутов, которые соответствуют любому количеству сегментов URL Используйте сегмент общего захвата 15.23-15.24 Ограничение URL, которым может соответствовать маршрут Определение маршрута внутри контроллера Применяйте ограничения маршрута 15 .25-15.34 Используйте маршрутизацию 15.35-15.39 с помощью атрибутов Подготовка проекта для примера Для целей этой главы создайте новый проект типа Empty (Пустой) по имени Ur l sAn dRoutes с применением шаблона ASP.NET Core Web Applicatioп (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте обязательные пакеты NuGet в раздел dependenc ie s файла pro j ec t. j s on и настройте инструментарий Razor в разделе t ool s , как показано в листинге 15.1. Разделы, которые не нужны для данной главы, понадобится удалить. Листинг 15. 1. Добавление пакетов в файле proj ect. j son "dependencies ": { "Microsoft.NETCore.App ": "ver sion": "1.0 .0", "type ": "plat f o rm" }1 "Microsoft.AspNetCore.Diagnostics": "1 .0 .0", "Microsoft.AspNetCore.Server . IISi ntegrat ion": "1.0 .0", " Microsoft. AspNetCor e . Serve r . Kestr el": "1.0. 0 ", "Mic r os oft .Exte ns i ons.Logging.Cons ole": "1.0. 0 ", "Microsoft. AspNetCore. Mvc" : "l. О . О" , "Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 , "Microsoft.AspNetCore.Razor.Tools": } }1 "version": "l.0 .0-preview2-final", "type" : "build"
Глава 15 . Маршрутизация URL 429 "tools": { "Microsoft.AspNetCore.Razor.Tools": "1.0 .0 -preview2-final", "Microsoft.AspNetCore . Server.IISintegration.Tools": "l. 0 . 0 -preview2 -final " }, "frameworks": { " netcoreappl.O ": "imports": [ " dotnet5 . 6", "portaЬle - net45+winB " ] }, " buildOptions ": " emitEntryPoint ": true , " preserveCompilationContext ": true Б листинге 15.2 приведен масс Startup, который конфигурирует средства, пре­ доставляемые добавленными пакетами NuGet. Листинг 15.2 . Содержимое файла Startup. cs using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Dependencyinjection ; namespace UrlsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvc(); Создание класса модели Все усилия в настоящей главе снонцентрированы на сопоставление URL запросов с действиями . Единств енному имеющемуся классу модели передаются детал и о конт­ роллере и методе действия, которые были выбраны для обработки запроса. Создайте папку Models и добавьте в нее файл класса по имени Resul t. cs с определением, пока зан ным в листинге 15.3 . Листинг 15.З. Содержимое файла Resul t. cs из папки Models using System . Collections . Generic ; namespace UrlsAndRoutes . Models { puЬlic class Result { puЫic string Controller { get; set ; puЫic string Action { get; set ; }
430 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC puЫic I Dicti onary<string , object> Data ( get; } = new Dicti o nary<stri ng , object>() ; Свойства Cont r o l ler и Ac t ion будут использоваться для у1<азания того, как за ­ прос был обработан, а словарь Da ta - для хранения других деталей о запросе, выда­ ваемых сист е мой маршрутизации. Создание контроллеров Для демонстрации работы маршрутизации нам нужны про ст ые контроллеры. Соз­ дайте папку Control lers и добавьте в нее файл класса по им ени Ho meController . cs с содержимым из листинга 15.4. Листинг 15.4 . Содержимое файла HomeController . cs из папки Controllers u sing Microso ft. AspNetCore . Mvc ; using Url sAndRoutes .Model s ; names p ace UrlsAndRo u tes . Co n trollers puЫic class HomeControll e r : Controlle r puЫic ViewResult Index() => View("Result ", new Result ( Control l e r = nameof(HomeControl l e r ) , Act ion = nameof(Index) }); Метод действия Index () , определенный в контроллере Home , вы з ыва е т метод View ( ) для визуализации представления по имени Resul t (которое определяется в следующем ра зделе) и передает Resu l t в качестве объекта модели. Свойств а объ е кта модели устанавливаются с применением операции n ameof; они будут исполь з оваться для указания , какие контроллер и метод действия были задействованы для обслуж и­ вания запроса. Аналогично тому, как было описано выше, добавьте в папку Contro ll ers файл класса Cu st o merControl l er . cs с определением контроллера Customer (листинг 15.5). Листинг 15.5 . Содержимое файла Customercontroller.cs из папки Controllers usi n g Mi crosoft . AspNetCore . Mvc ; using Ur l sAndRoutes . Models ; name sp ace Ur l sAndRoutes . Co n trol l ers puЫic clas s Cu stomerContr oller : Controller puЫic ViewResult Index() => View("Result", new Result { Cont r ol ler = n a meof(CustomerContro l ler} , Acti on = nameof (Index) });
Глава 15 . Маршрутизация URL 431 puЫic ViewResul t List () => View ( " Resul t" , new Result { Controller = nameof(CustomerController) , Action = nameof(List) )); Посл едний третий контроллер определен в файле по имени AdminController . cs внутри папки Controllers (листинг 15.6), который добавлен так, как демонстриро­ валось ранее. Листинг 15.6 . Содержимое файла AdminController.cs из папки Controllers using Microsoft . AspNetCore . Mvc; using UrlsAndRoutes . Models ; namespace UrlsAndRoutes . Controllers puЬlic class AdminController : Controller puЫic ViewResult Index() => View( " Result ", new Result { Controller = nameof(AdminController) , Action = nameof(Index) }); Создание представления Во всех методах действий, определенных в предыдущем разделе, указано представ­ ление Resul t, что позволяет создать одно представление, которое будет разделяться всеми контроллерами. Создайте папку Views/Shared и добавьте в не е новый файл представления по имени Resul t. cshtml с содержимым, показанным в листинг е 15.7 . Листинг 15.7. Содержимое файла Resul t. cshtml @model Result @{ Layout = null ; <IDOCTYPE html> <html> <head> <meta name= " viewport " content="width=device - width " /> <title>Routing</title> <link rel= " stylesheet " asp- href-include= " lib/bootstrap/dist/css/*.min.css" /> </head> <body class= " panel-body " > <tаЫе class= " taЫe taЬle -b ordered taЫe - striped taЬle-condensed"> <tr><th>Controller : </th><td>@Model . Controller</td></tr> <tr><th>Action : </th><td>@Model . Action</td></tr> @foreach (string key in Model . Data .Keys) { <tr><th>@key : </th><td>@Model . Data[key]</td></tr> } </tаЫе> </body> </html>
432 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC Представление содержит стилизованную с помощью Bootstrap таблицу, которая отображает свойства из объекта модели. Чтобы добавить Bootstrap в проект, создайте файл bower. j son, используя шаблон элемента Bower Configuration File (Файл конфи­ rурации Bower), и укажите пакет Bootstrap в разделе dependencies (листинг 15.8). Листинг 15.8 . Добавление пакета Bootstrap в файле bower. j son "name": "asp.net ", "priva te": true, "dependencies ": { "bootstrap": "3. 3 . 6 11 Последний подготовительный шаг предусматривает создание в папке Views файла _ Viewimports . cshtml, в котором настраиваются встроенные дескрипторные вспо­ могательные классы для применения в представлениях Razor и импортируется про­ странство имен модели (листинг 15.9). Листинг 15.9. Содержимое файла_Viewimports. cshtml из папки Views @using UrlsAndRoutes.Models @addTagHelper * , Microsoft.AspNetCore . Mvc . TagHelpers Конфигурация в классе Startup не содержит никаких инструкций относительно того, как инфраструктура MVC должна сопоставлять Н1ТР-запросы с контроллерами и действиями. После запуска приложения любой запрашиваемый URL будет давать в результате ответ 404 - Not Found, как показано на рис. 15.1 . Рис. 15. 1. Запуск примера прило жения Введение в шаблоны URL Система маршрутизации работает с использованием набора маршрутов. Вместе эти маршруты образуют схему URL или схему для приложения, представляющую со­ бой набор URL, которые приложение будет распознавать и реагировать на них. Набирать вручную все индивидуальные URL, которые планируется поддерживать в приложении, не придется. Взамен каждый маршрут содержит шаблон URL, с которым сравниваются входящие URL. Если URL соответствует шаблону, тогда он применяется системой маршрутизации для обработки этого URL. Начнем с простого URL: http://mysite.com/Admin/Index
Глава 15 . Маршрутизация URL 433 URL могут быть разбиты на сегменты - части URL за исмючением имени хоста и строки запроса, rюторые отделяются друг от друга символом / . В приведенном выше примере URL есть два сегмента, как по1<азано на рис. 15 .2. http://mysite.com/Admin/Index tt Первый Второй сегмент сегмент Рис . 15.2 . Сегменты в примере URL Первый сегмент содержит слово Admin, а второй - слово Index . В глазах чело­ века совершенно очевидно , что первый сегмент имеет отношение к контроллеру, а второй - к действию. Однако, естественно, такое отношение должно быть выражено с использованием шаблона URL, который может быть воспринят системой маршру­ тизации. Вот шаблон URL, соответствующий примеру URL: {controller}/{action} При обработке входящего НТГР-запроса задача системы маршрутизации заключа­ ется в сопоставлении запрошенного URL с шаблоном и извлечении из URL значений для переменных сегментов, определенных в шаблоне. Переменные сегментов выражаются с применением фигурных скобок (символов [ и }). В по1шзанном выше примере шаблона присутствуют две переменные сег ­ ментов с именами controller и action, так что значением переменной сегмента controller будет Admin , а значением переменной сегмента act i on - Index. Приложение МVС обычно будет иметь несколько маршрутов, и система маршру­ тизации будет сравнивать входящий URL с шаблоном URL каждого маршрута до тех пор, пока не найдет совпадение. По умолчанию шаблон URL будет соответствовать любому URL, который имеет подходящее количество сегментов. Например, шаблон {controller }/ {action} будет соответствовать любому URL с двумя сегментами, как описано в табл. 15.3 . Таблица 15.3 . Сопоставление URL URL запроса Переменные сегментов http://mysite.com/Admin/Index controller=Admin action= Index http: / /mysi te. com/Admin Соответствия нет - сегментов слишком мало http: / /mysi te . com/Admin/Index/Soccer Соответствия нет - сегментов слишком много В табл . 15.3 отражены две мючевых линии поведения шаблонов URL. • Шаблоны URL консервативны в отношении количества сопоставляемых сегмен­ тов. Совпадение будет происходить только для тех URL, которые имеют то же самое количество сегментов, что и шаблон. Это можно наблюдать во втором и третьем примерах в таблице. • Шаблоны URL либеральны в отношении содержимого сопоставляемых сегмен­ тов. Если URL имеет правильное количество сегментов , то шаблон извлечет зна­ чение каждого сегмента для переменной сегмента, каким бы оно ни было.
434 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Описанные две стандартных линии поведения являются ключом к пониманию функционирования шаблонов URL. Позже в главе будет показано, как изменить эти стандартные линии поведения. Создание и регистрация простого маршрута Имеющийся шаблон URL можно использовать для определения маршрута. Маршруты определяются в файле Start u p. cs и передаются в кач е стве аргументов методу UseMvc (), который применяется для настройки MVC в методе Configure (). В листинге 15.10 приведен базовый маршрут, отображающий запросы на контролле­ ры в примере приложения. Листинг 15.1 О. Определение базового маршрута в файле s tartup. cs us ing Microsoft.AspNetCore .Builder; using Micr osof t .Extensions .Dependencyinj ection ; nam es pace Url s AndRoutes { puЫic class Startup { pu Ьlic void Con fig ureServi ce s ( I Se r viceCollection services) { s ervices.AddM vc(); puЬl ic voi d Configure(IApplicat i onBu ilder а р р) { a pp . UseStatusCodePages() ; app .UseDevel ope rExceptionPage (); app .Us e Stati cFi les() ; арр. UseМvc (routes => { routes.МapRoute(name: "default", template: "{controller}/{action}"); }); Маршруты создаются с использованием лямбда-выражения, передаваемого в виде аргум ента конфигурационному методу UseMvc () . Это выражени е получает объект, который реализует интерфейс IRout e Bu ilder из пространства имен Microsoft . AspN etCo r e . Rout i ng , а маршруты определяются с применением расширяющего метода Ma p Ro u te () . Чтобы облегчить понимание маршрутов, по соглашению при вызове метода MapRoute () указываются имена аргументов. что объясня ет наличие в листинге явно именованных аргументов n am e и temp late. В аргументе name указы­ вается имя маршрута, а в аргументе template определяется шаблон. Совет. Назначать маршрутам имена не обязательно, и существует философский аргумент отно­ сительно того, что именование приносит в жертву четкое разделение обязанностей, которое в противном случае обеспечила бы маршрутизация. В разделе " Генерация URL из специфи­ ческого маршрута" главы 16 объясняется, почему именование может стать проблемой . Запустив пример приложения, можно увидеть эффект от изменений, внесенных в маршрутизацию. При первом запуске приложения ничего не изменится-вы увидите ошибку 404 , но если перейти на URL , который соответствует шаблону {controller} / {act ion} , то получится результат, подобный показанному на рис . 15.3, где иллюст­ рируется переход на /Admin/I n d e x .
Глава 15. Маршрутизация URL 435 Controller: Adm lnController Action : lnd ex Ри с. 15.З. Навигация с использованием простого маршрута Корневой URL для приложения не работает из-за того, что маршрут, добавленный в файле Startup. cs, не сообщает инфраструктуре МVС о том, как выбирать класс контроллера и метод действия, когда URL запроса не содержит сегментов . Мы испра­ вим это в следующем разделе. Определение стандартных значений Пример приложения возвращает ошибку 404, когда запрашивается стандарт­ ный URL, поскольку он не соответствует шаблону маршрута, определенного в классе Startup. Из-за отсутствия в стандартном URL сегментов, 1шторые могли бы соответс­ твовать переменным controller и action, определенным в шаблоне маршрута, сис­ тема маршрутизации не дает совпадения. Ранее объяснялось, что шаблоны URL будут соответствовать только URL с указан­ ным количеством сегментов. Один из способов изменить такое поведение предусмат­ ривает применение стандартных значений. Стандартное значение используется, когда URL не содержит сегмент, который можно было бы сопоставить с шаблоном маршрута . В листинге 15. 11 определяется маршрут, использующий стандартное значение. Листинг 15.11 . Предоставление стандартного значения в файле Startup. cs using Microsoft . AspNetCore .Builder; using Microsoft.Extensions.Dependencylnjecti on ; na mespace UrlsAndRou tes { puЬlic class Startup { puЫic void ConfigureServices(IServi ceCollection services) { services . AddMvc() ; puЬlic void Conf igure(IApplicationBuilder арр) { app .Us eStatusCodePages() ; app . UseDeveloperExceptionPage(); app.UseStaticFiles() ; арр . UseМvc (routes => { routes .MapRoute ( name: "default", }); template: "{controller}/{action}", defaul ts: new { action = "Index" } ) ;
436 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Стандартные значения задаются как свойства анонимного типа, передаваемого методу MapRoute () в аргументе defaul ts. В листинге 15.11 для переменной action предоставлено стандартное значение Index. Как и ранее, этот маршрут будет соответствовать всем двухсегментным URL. Например, в случае запроса URL вида http://mydomain.com/Home/Index маршрут извлечет Home в качестве значения для controller и Index - для act i on. Но теперь, когда имеется стандартное значение для сегмента action, маршрут будет также соответствовать и односегментным URL. При обработке односегментно­ го URL система маршрутизации извлечет значение для переменной controller из URL и будет применять стандартное значение для переменной action. Таким обра­ зом, запрос пользователем /Home приведет к тому, что МVС вызовет метод действия Index () контроллера Home (рис. 15.4). Controll er: AdminControlle r Action: lndex Рис. 15.4. Использование стандартного действия Определение встроенных стандартных значений Стандартные значения могут также быть выражены как часть шаблона URL, что дает более лаконичный способ определения маршрутов, как показано в листин­ ге 15.12 . Встроенный синтаксис может применяться только для предоставления стандартных значений переменным, которые являются частью шаблона URL, но, как вскоре вы узнаете, зачастую удобно иметь возможность устанавливать стандартные значения за пределами шаблона. По этой причине важно понимать оба способа вы­ ражения стандартных значений. Листинг 15.12. Определение встроенных стандартных значений в файле Startup. cs using Microsoft . AspNetCore .B uilder ; using Microsoft . Extensions.Dependency!njection ; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services . AddMvc(); puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app .U seStaticFiles() ; app . UseMvc(routes => { routes.MapRoute (name : " default ", template: "{controller}/{action=Index}"); });
Глава 15. Маршрутизация URL 437 Мы можем пойти еще дальше и сопоставлять с URL, которые вообще не содержат переменных сегментов , полагаясь при идентификации действия и контроллера толь­ ко на стандартные значения. Для примера в листинге 15.13 демонстрируется отоб­ раже ни е корневого URL прилож ения за счет предоставления стандартных знач ений для обоих сегм ентов. Листинг 15.1 З. Предоставление стандартных значений для переменных controller и action в файле Startup . cs using Microsoft . AspNetCore . Bui lder ; using Microsoft.Ex t ensions . Depe nd e ncyi nj ection ; namespace Ur l sAndRoutes { p u Ыic class Startup { puЬlic void ConfigureServi ces(IS e r viceCo l lect i on s e rvi ces ) { services . AddMvc() ; puЫic vo i d Co nf igu re(IApplicati o n Buil der арр) { app . OseStat u sCodePages() ; app . UseDeveloperExce p tionPage() ; app . UseStaticFiles() ; app . OseMvc(routes => { routes . MapRou t e( name : "default", template : "{controller=Home}/{action=Index}"); }); За сч ет предоставл ения стандартных значений для переменных controller и action маршрут будет соотв етствовать URL с нулем , одним или двумя сегм ентами (табл . 15 .4). Таблица 15.4 . Соответствие URL Количество сегментов о 2 з Пример / /Customer /Customer/List /Customer/List/All На что отображается cont r o ller=Home action=Index controller=Customer ac tio n= Index contr oll e r =Customer acti on=Li st Соответствия нет - сегментов слишком много Ч ем меньше сегментов получ е но во входящем URL, тем больше маршрут полагает­ ся на стандартные значения, вплоть до момента, когда URL без сегментов сопостав­ ляется с использованием только стандартных значений. З апустив пример приложения, можно увидеть результат применения стандартных значений . Когда бр аузер запрашивает корневой URL приложения, для переменных сегментов control l er и act i on будут использоваться стандартные значения, ко­ торые прив едут к тому , что инфраструктура MVC вызовет метод действия I ndex () контроллера ·Ноте (рис. 15.5).
438 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC D Routing Х f- - С ГФ lo~Jh~~\~s2994====~~=-=-!:J : 1 . -· ---· --·- - " ---- -- -- --- -· - ---·--- ---- --· ---1 Controller: HomeCont roller li Action : lndex l____________________" _________ -- ________J Рис. 15.5. Применение стандартных значений для расширения области действия маршрута Использование статических сегментов URL Не все сегменты в шаблоне URL должны быть переменными. Допускается ТЮ{Же со­ здавать шаблоны , имеющие статические сегменты. Предположим . что приложение нуждается в сопоставлении с URL, которое снабжены пр ефиксом PuЫic, например: http : //mydomain . com/PuЬlic/Home/ Ind ex Это можно сделать с применением шаблона URL , подобного показанн ому в лис­ тинге 15.14. Листинг 15.14 . Шаблон URL со статическими сегментами в файле Startup. cs using Microsoft.AspNetCore.Builder ; using Microsoft . Extensions . Dependencyinjection ; namespace UrlsAndRoutes { puЫic c l ass Startup { puЫic void ConfigureServices(IServiceCol l ection services) { services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app .Us eStaticFiles() ; app . UseMvc(routes = > { routes . MapRoute( name : " defaul t", template : "{ controller=Home}/{action=Index} " ) ; routes. мapRoute (name: 11 11 , tempJ.ate: "PuЫic/{controJ.J.er=Home}/{action=Index}"); }); Новый шаблон будет соответствовать только URL, содержащим три сегмента, пер­ вым из которых должен быть PuЬlic . Остальные два сегмента могут содержать лю­ бые значения, и они будут использоваться для переменных controller и a ction. Если последние два сегмента не указаны. тогда будут применяться стандартные значения.
Глава 15. Маршрутизация URL 439 Можно также создавать шаблоны URL, которые имеют сегменты, содержащие как статические, так и переменные элементы, вроде шаблона в листинге 15 . 15. Листинг 15.15. Шаблон URL со смешанным сегментом в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft .E xtensions.Dependencyinjection ; namespace UrlsAndRoutes puЫic class Startup { puЬlic void ConfigureServices(IServiceCollection services) { services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages(); app.UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvc(routes = > { routes .MapRoute ( 1111 , 11 Х{ controller} / { action} 11 ) ; routes . MapRoute( name : 11 default 11 , template: 11 {controller=Home}/{action=Index} 11 ); routes. MapRoute (narne : "", template : 11 P uЬlic/{controller=Home}/{action =I ndex} " ) ; }); Шаблон в показанном маршруте соответствует любому двухсегментному URL , в котором первый сегмент начинается с буквы Х . Значение для controller берется из первого сегмента, исключая Х. Значение для action получается из второго сегмента. Запустив приложение и перейдя на URL вида /XHome/Index, можно увидеть эффект от добавления этого маршрута (рис. 15.6). D Routing f- - С ГФ localt;~st 52994/Xнome/111dex -- lndex 1 Controller: с HomeController Ри с. 15.6. Смешивание в одном сегменте статических и переменных элементов
440 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Упорядочение маршрутов В листинге 15.15 новый маршрут был определен и помещен перед другими маршрутами . Так было сделано потому, что маршруты применяются в порядке, в котором они определе­ ны . Метод MapRoute () добавляет маршрут в конец конфигурации маршрутизации, а это означает, что обычно маршруты применяются в порядке их определения. Мы сформулиро­ вали "обычно" , т.к . есть методы, которые позволяют вставлять маршруты в специфические позиции. Я стараюсь не использовать такие методы, поскольку применение маршрутов в порядке их определения упрощает понимание маршрутизации в приложении. Система маршрутизации пытается сопоставить входящий URL с шаблоном URL маршрута, который был определен первым , и продолжает сопоставление со следующим маршрутом только в случае, если не произошло совпадение. Маршруты проверяются последователь­ но, пока не обнаружится соответствие или не исчерпается набор маршрутов. Вследствие этого самые специфичные маршруты должны быть определены первыми. Маршрут, добав­ ленный в листинге 15 .15, является более специфичным, чем следующие за ним маршруты . Предположим, что порядок следования маршрутов изменен на обратный : routes . MapRoute( " MyRoute ", " {contro ller=Home}/{action=Index} " ) ; routes . MapRoute( "", " X{controller}/ {action} " ) ; Тогда первый маршрут, который соответствует любому URL с нулем, од ним или двумя сег­ ментами, будет использоваться всегда. Более специфичный маршрут, который теперь вто­ рой в списке , никогда не будет достигнут. Новый маршрут исключает ведущую букву х из URL, но это не будет сделано из-за совпадения со старым маршрутом. Таким образом, URL такого вида : http : //mydomain . com/XHome/Index будет нацелен на контроллер по имени XHome, предполагая существование в приложении класса XHomeController и наличие в нем метода действия Index (). Статические сегменты URL и стандартные значения можно 1юмбинировать с це­ лью создания пс евдонима для специфического URL. После развертывания приложе­ ния применяемая схема URL формирует контракт с пользователями , и при последую­ щем рефакторинге приложения необходимо предохранить предыдущий формат URL , чтобы любые URL, добавленные в закладки, макросы или сценарии, написанные пользователем, продолжали работать. Представим. что у нас использовался контроллер по имени Shop, который те перь заменен контроллером Horne . В листинге 15.16 показано, как со здать маршрут для предохранения старой схемы URL. Листинг 15.16. Смешивание статических сегментов URL и стандартных значений в файле Startup. cs using Microsoft.AspNetCore . Builder ; us i ng Microsoft . Extensions . Dependencyinjection ; namespace UrlsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc() ;
Глава 15. Маршрутизация URL 441 puЫic void Configure(IApplicationBuilder арр) { app .Us eStatusCodePages(); a pp .U seDeveloperExceptionPage(); a pp . UseSta ticFiles(); app.UseMvc (routes => { routes. MapRoute (name: "ShopSchema", template : "Shop/ {action} " , defaul ts: new { controller = "Ноте" }) ; routes . MapRoute(" ", "X{controller}/{action}"); routes.MapRo ute( name: " default" , templ a t e : "(controller= Home}/{action=Index}"); routes.MapRoute(name: "", template: "PuЬlic/{controller=Home}/{action=Index}"); }); Добавленный маршрут соответствует любому двухсегментному URL. в котором первым сегментом является Shop. Значение action берется из второго сегмента URL. Шаблон URL не содержит переменного сегмента для con troller, поэтому задейству­ ется стандартное значение. Аргумент defaults предоставляет значение c o ntroller, потому что отсутствует какой-либо сегмент, к которому можно было бы применить значение как часть шаблона URL. В результате запрос действия из контроллера Shop транслируется в запрос для контроллера Home. Запустив приложение и перейдя на URL вида /Shop/Index, мож­ но увидеть эффект от добавления такого маршрута . Как показано на рис. 15.7, но­ вый маршрут приводит к тому, что инфраструктура МVС вызывает метод действия Index () из контроллера Ноте . С) Routing х r---- ------"'== ~ С ~ loca lhost:S2:i94/Shop/lndr.>: . -----------··· =====::::::: 1 Controlle r: 1 Act/On' HomeCo ntrol le r lnde)( 1___-·----·-------- ------------------·------ Рис. 15.7. Создание псевдонима для предохранения схемы URL Можно продвинуться на шаг дальше и создать псевдонимы также для методов действий, которые в результате рефакторинга перестали существовать в контролле­ ре. Чтобы сделать это, понадобится просто создать статический URL и предоставить стандартные значения для contro ller и action (листинг 15.17).
442 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 15.17. Создание псевдонима для контроллер а и действия в файле Startup . cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions . Dependencyinjection ; namespace Ur lsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCol lection s ervices) { services . AddMvc() ; puЫic void Configure( I ApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app.UseStat i cFiles() ; app.UseMvc(routes = > { routes.MapRoute(name: "ShopSchema2", template : "Shop/OldAction", defaul.ts : new { control.1 .er = "Home", action = "Index" }) ; routes . MapRoute(name : " ShopSchema ", template : " Shop/{action} ", defaults : new { controller = "H om e" }) ; routes . MapRoute( "", " X{controller}/{act i onl " ) ; route s. Map Route( name: "def ault", template : " {control l er=Home)/{action =I ndexl " ) ; routes . MapRoute (name : "", template : " PuЬlic/ { controller=Home}/{action = Index} " ) ; }); Обратите внимание, что новый маршрут определен первым, поскольку он являет­ ся более специфичным , чем маршруты, следующие после него. Если запрос для Shop/ OldAction обр аботается , к примеру, следУющим определенным маршруто м , тогда может быть получен результат, отличающийся от того, который желательно получить при наличии контролл е р а с м ет одом действия OldAction () . Определение специальных переменных сегментов Перем енные сегментов contro ll er и action имеют специальное наз нач ени е в прилож ениях MVC и соответствуют контроллеру и методу действия, которы е будут использоваться для обслуживания запроса . Они являются всего л ишь встроенными п е р еменны м и сег м ентов , и допускается также определять специальные п е рем е нны е с егм ентов , как показано в листинге 15.18. (Маршруты, определенные в предыдущем разделе, удалены, чтобы можно было н ачать сначал а .)
Глава 15 . Маршрутизация URL 443 Листинг 15.18. Определение дополнительных переменных внутри шаблона URL в файле Startup. cs using Microsoft.AspNetCore.Builder ; using Microsoft . Extensions . Dependencyinjection ; namespace UrlsAndRoutes puЫic class Startup ( puЫic void ConfigureServices(IServiceCollection services) ( services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles(); app.UseMvc(routes => { rou tes . MapRoute (name : "MyRoute" , template: "{controller=Home}/{action=Index}/{id=Defaultid}"); }); Шаблон URL опр едел яет стандартные переменные controller и act i on, а так­ же специальную п е р еменную по имени id. Маршрут буд ет соответствовать URL с количеством сегментов от нуля до трех . Содержимое третьего сегмента присваива­ ется п ер емен ной id, а при отсутствии третьего сегмента применяется стандартное значение. Внимание! Не которые имена зарезервированы и не доступны для использования в качест­ ве имен специальных переменных сегментов. К ним относятся controller, action и area. Назначение первы х двух очевидно, а области обсуждаются в следующей главе . В классе Controller, который является базовым для контроллеров, опред е ле­ но свойство RouteData . Это свойство во з враща ет объект Microsoft . AspNetCore . Routing . RouteData, предоставляющий детали о системе маршрутизации и способе, которым был маршрутизирован текущий запрос. Внутри контроллера можно полу­ ч ать доступ к любой переменной сегмента в методе действия с применением свойс­ тва RouteData . Values, которое возвращает словарь, содержащий переменные сег­ мента. В цел ях демонстрации в контроллер Home добавлен метод дей ствия по имени CustomVariaЫe (), приведенный в листинге 15.19. Листинг 15.19. Доступ к специальной переменной сегмента внутри метода действия в файле HomeController. cs using Microsof t. AspNetCore . Mvc ; using UrlsAndRoutes . Mod els; namespace UrlsAndRoutes . Controllers puЫic class HomeContro l ler : Contro l ler
444 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC puЫic ViewResult Index() => View( " Result ", new Result { Cont r olle r = nameof(HomeController), Action = nameof(Index) }); puЫic ViewResult CustornVariaЬle() Result r = new Result { Controller = narneof(HorneController), Action = narneof(CustornVariaЬle), }; r. Data [ "id"] = RouteData. Values [ "id"] ; return View("Result", r); Этот метод действия получает значение специальной переменной i d в шаблоне URL маршрута, используя свойство RouteData . Values, которое возврашает словарь переменных, порожденных системой маршрутизации. Специальная переменная до­ бавляется к объекту модели представления и может быть просмотрена путем запуска приложения и запроса следующего URL: /Home/CustomVar i aЫe/Hello Шаблон маршрута распознает третий сегмент в данном URL как значение для пе­ ременной id, давая приведенный на рис. 15.8 результат. CJ Rou ting х Controller: HomeController Action : CustomVari aЫe Рис. 15.8 . Отобра же ние значения специальной переменной сегмента Шаблон URL в листинге 15 . 19 определяет стандартное значение для сегмента id, т.е. маршрут может также соответствовать URL, которые имеют два сегмента. Увидеть применение стандартного значения можно, запросив такой URL: /Home/CustomVariaЫe Система маршрутизации использует стандартное значение для специальной пере­ менной, как показано на рис. 15.9 .
Глава 15 . Маршрутизация URL 445 D Routin9 Controller: Action : id: х HomeController CustomVariaЫe Defaultld _J Рис. 15.9. Стандартное значение для специальной переменной сегмента Использование специальных переменных в качестве параметров метода действия Работа с коллекцией RouteData . Values - лишь один способ доступа к специаль­ ным переменным маршрута, а другой способ может быть намного более элегант ным. Если метод действия определяет параметры с именами, которые совпадают с имена­ ми переменных шаблона URL, то инфраструктура МVС будет автоматически переда­ вать методу действия значения, извлеченные из URL, в виде аргументов. Специальная переменная, определенная в маршруте из листинга 15.18, имела имя id. Модифицируйте метод действия CustomVariaЫe () в контроллере Home , так что­ бы он принимал параметр с тем же самым именем (листинг 15.20). Листинг 15.20. Добавление параметра к методу действия в файле HomeCon troller. cs using Microsoft . AspNetCore . Mvc; using UrlsAndRoutes . Models ; namespace UrlsAndRoutes . Controllers puЫic class HomeController : Controller puЬlic ViewResult Index() => View( "Resul t ", new Result { Controller = nameof(HomeController) , Action = nameof(Index) }); puЬlic ViewResul t CustomVariaЫe ( string id) Result r = new Result { Controller = nameof(HomeController) , Action = nameof(CustomVariaЫe), }; r .Data [ "id"] = id; return View( " Result ", r) ;
446 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Когда система маршрутизации обнаруживает соответствие какого-то URL марш­ руту, который был определен в листинге 15.18, значение третьего сегмента URL при­ сваивается специальной переменной id. Инфраструктура MVC сравнивает список пе­ ременных сегментов со списком параметров метода действия, и в случае совпадения имен передает значения из URL этому методу. Типом параметра id является string, но MVC будет пытаться преобразовать зна­ чение из URL в любой тип, указанный для параметра. Если параметр id метода дейс­ твия определен как int или DateTime, тогда значение из URL будет получено в виде экземпляра заданного типа . Это элегантное и удобное средство, которое избавляет от необходимости обрабатывать преобразования самостоятельно. Запустив приложение и запросив URL вида /Home/CustomVariaЫe/Hello, можно увидеть эффект от па­ раметра метода (рис. 15.10). Если опустить третий сегмент, то методу действия будет предоставлено стандартное значение для переменной сегмента, которое также видно на рис. 15.10. D Rcuting Х !) Rou ting Х .\' -~----- ~ _@-~~~~~ome~~~~-~~~~~~~~-~ --°. '-~-~calh~s~~~:~~-":_~~~~-m~~Ыe _ •.:~ '-1 :::'" ::=~:: 1 :::'"' ::::,::: 1 ~-------- ___J ___________________________ J Рис. 15.1 О. Доступ к переменным сегментов с применением параметров метода действия На заметку! Для преобразования значений, содержащихся в URL, в типы .NET инфраструк­ тура MVC использует средство привязки моделей, которое может обрабатывать намно­ го более сло жные ситуации, чем продемонстрированная в приведенном выше примере. Привязка моделей будет описана в главе 26. Определение необязательных сегментов URL Необязателы;_ым является такой сегмент URL, который пользователь может не указывать, и для которого не предусмотрено стандартного значения. Необязательный сегмент обозначается знаком вопроса (символом ?) после имени сегмента, как пока­ зано в листинге 15.21 . Листинг 15.21. Определение необязательного сегмента URL в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions . Dependency!njection ; namespace UrlsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc () ;
Глава 15 . Маршрутизация URL 447 puЫic void Configure(IApplicationBuilder арр) { app.OseStatusCodePages() ; app.OseDeveloperExceptionPage(); app . OseStaticFiles() ; app . OseMvc(routes => { routes. MapRoute (name: "M yRoute ", template: "{controller=Home}/{action=Index}/{id?}"); }); Данный маршрут будет соответствовать URL, которые имеют или не имеют сег­ мента id. В табл. 15 .5 демонстрируется его работа с различными URL. Таблица 15.5 . Соответствие URL с необязательной переменной сегмента Количество сегментов Пример URL На что отображается о 2 3 4 / /Customer controller=Home action=Index controller=Customer action=Index /Customer/List controller=Customer action=List /Customer/List/All controller=Customer action=List id=All /Customer /List/All/Delete Соответствия нет - сегментов слишком много В табл. 15 .5 видно, что переменная id добавляется к набору переменных, только когда во входящем URL присутствует соответствующий сегмент. Такая возможность удобна, когда необходимо узнать, предоставил ли пользователь значение для пере­ менной сегмента. Если для необязательной переменной сегмента значение не было задано, тогда значением соответствующего параметра будет null. В листинге 15 .22 приведено обновл ение контроллера Home в целях уч ета ситуации , когда для перемен­ ной сегмента id значение не указано. Листинг 15.22 . Проверка необязательной переменной сегмента в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using Or l sAndRoutes . Models ; namespace OrlsAndRoutes . Controllers puЫic class HomeController : Controller puЫic ViewResult Index() => View( " Result ", new Result { Controller = nameof(HomeControlle r ) , Act i on = nam eof( Index) }); puЫic ViewResult CustomVar i aЬle(string id) { Result r = new Result { Controller = nameof(HomeController), Action = nameof(CustomVariaЬle) , };
448 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC r.Data["id"] = id ?? "<no value>"; return View( " Resu l t", r); На рис . 15.11 показан результат запуска приложения и перехода по URL вида /Home/CustomVariaЬle, в котором не включено значение для переменной сегмента id. Controller: HomeCoпtroll e r Action : CustomVariaЫe id: <ПО value> 1 L------·--·--· -··--····-·-·--·---------·--· - ·-·--- _____ J Рис. 15.11. Обнаружение ситуации, когда URL не содержит значения для необязательной переменной сегмента Стандартная конфигурация маршрутизации Добавление инфраструктуры MVC в классе Startup осуществляется с применением ме­ тода UseMvcWithDefaultRoute (). Он представляет собой просто удобный метод для настрой ки наиболее распространенной конфигурации маршрутизации и эквивалентен сле­ дующему коду: app . UseMvc(routes => routes . MapRoute( name: " default ", template : "{ co ntroller=Home}/{action=Index} /{ id?}" ) ; }); Такая стандартная конфигурация обеспечивает сопоставление с URL, которые нацелены на классы контроллеров и методы действий по их именам, а также могут содер жать необяза­ тельный сегмент id. Если сегмент controller или action опущен, тогда используют­ ся стандартные значения, чтобы направить запрос контроллеру Ноте и методу действия Index () . Определение маршрутов переменной длины Еще один способ изменения консервативного стандартного поведения шаблонов URL предусматривает прием переменного количества сегментов URL. Это позволяет маршрутизировать URL произвольно й длины в единственном маршруте. Поддержка
Глава 15. Маршрутизация URL 449 переменного числа сегментов определяется путем назначения одной из переменных сегментов в качестве переменной общего захвата, для чего она предваряется симво­ лом звездочки (листинг 15.23). Листинг 15.23. Назначение переменной общего захвата в файле startup. cs using Microsoft . AspNetCore .Builder ; using Microsoft . Extensions .De pendencyinjection ; namespace UrlsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCol l ection servi ces) { services. AddMvc (); puЫic void Co n figure( IAppli cat ionBuilder арр) { app .UseStatusCodePag es(); app .Use DeveloperExceptionPage() ; app . UseStaticFiles() ; app.UseMvc(routes => { routes. MapRoute (name: "M yRoute ", template: "{controller=Home}/{action=Index}/{id?}/{*catchall}"); }); Здесь был расширен маршрут из предыдущего примера за счет добавления пере­ менной общего захвата по имени catchall . Теперь этот маршрут будет соответство­ вать любому URL вне зависимости от количества содержащихся в нем сегментов либо их значений . Первые три сегмента применяются для установки значений перемен­ ньrх controller, action и id. Если URL содержит дополнительные сегменты, то все они присваиваются переменной catchall, как показано в табл. 15.6 . Таблица 15.6 . Сопоставление URL с переменной общего захвата Количество сегментов о 2 3 4 5 Пример URL / /Customer /Customer/List /Customer/List/All /Customer/List/All/Delete /Customer/List/All/ Delete/Perm На что отображается controller=Home action=Index controll er=Customeraction=Index controller=Customeraction=List controller=Customer action=List id=All control l er=Customeraction=Listid=All catchall=Delete controller=Customer act i on= List id=A ll catchall= Delete/ Perm В листинге 15.24 приведен обновленный контроллер Customer, в котором дейс­ твие List передает представлению значение переменной catchall через объект модели.
450 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 15.24. Модификация метода действия в файле CustomerController.cs us ing Microsoft . AspNetCore . Mvc ; us i ng UrlsAndRoutes . Models ; narnespace UrlsAndRo ut es.Cont r o l lers puЬlic class Custorne rControl l er : Contro l ler puЫic ViewResult Index () => View( "Resul t ", new Result { Control l er = narneof(Custorne r Control l er) , Action = narneof(Index) }); puЫic ViewResult List(string id) { Result r = new Result { Controller = nameof(HomeController), Action = narneof(List), }; r. Data [ "id"] = id ?? "<no value>"; r.Data["catchall"] = RouteData.Values["catchall"]; return View("Result", r); Чтобы протестировать сегмент общего захвата , понадобится запустить приложе ­ ние и запросить сл едующий URL: / Cu stome r / Li st/Hel l o/1/2/3 Верхнего предел а количества сегментов , для которого шаблон URL в этом марш­ руте будет давать совпадение, не существует. На рис . 15 .12 показан э ффект от опр е­ деления сегмента общего захвата. Обратите внимание, что сегменты , попадающие в переменную общего захвата, представлены в форме се гмент/ сегмент/ сегмент , и мы сами отвечаем з а обработку строки для ее р а збиения н а отдельны е се гменты. D Rou tiog х f- С [Фlocalhost:52994/C~tomer/list/Hl?Ho/1f2/3 ·- -- - ·- *l - - -- ---~-..:--:":':".:=:=--:-:-:-=--:-:--=-:-·=--=-==-=-~-=-=-.--:::::--:-:=..~~ Controller: Acti on : id: catchal l : HomeControll er List Hello 1/2/3 1 _________ _ ______ _ ______________ ] Рис. 15. 12. Использование сегмента общего захвата
Глава 15. Маршрутизация URL 451 Ограничение маршрутов В начале главы было указано , что шаблоны URL консервативны в отношении коли­ чества сопоставляемых сегментов и либеральны в отношении содержимого сопостав­ ляемых сегментов. В нескольких предшествующих разделах объяснялись различные приемы управления степенью консервативности: увеличение или уменьшение числа сегментов с применением стандартных значений, необязательных переменных и т.д. Теперь наступило время взглянуть на то , каким образом управлять либеральнос­ тью при сопоставлении с содержимым сегментов URL, а именно - как ограничить набор URL, с которыми будет сопоставляться маршрут. В листинге 15.25 демонстри­ руется использование простого ограничения URL, с которыми будет сопоставляться маршрут. Листинг 15.25. Ограничение маршрута в файле Startup. cs using Microsoft.AspNetCore.Builder; using Microsoft . Extensions.Dependency inje ct i on ; namespace UrlsAndRoutes puЬlic class Startup { puЫic void Con figureSer v ices(IS erviceCol l ect i on services) { services . AddMvc() ; puЬlic void Conf i gure(IAppli cationBuilder арр) { app . UseStatusCodePages() ; app . UseDevelope rE xcept ionP age() ; app . UseStaticFiles() ; app . UseMvc(routes => { routes . MapRoute(name : " MyRoute ", template: "{controller=Home}/{action=Index}/{id:int?}"); }); Ограничения отделяются от имени переменной сегмента дво еточием (сим вол : ). Ограничением в листинге является int, и оно применяется к сегменту id. Это при­ мер встроенного ограничения, которое определено как часть шаблона URL и приме­ нено к одиночному сегменту: template: "{ controller}/{act ion}/{id:int?}", Ограничени е int разрешает сопоставление шаблона URL только с сегментами, значение которых может быть преобразовано в тип int. Сегмент id необязателен, так что маршрут будет сопоставляться с URL без сегмента id, но если он присутству­ ет, то должен быть целочисленным значением (в табл . 15.7 приведены примеры).
452 Часть 11. Подробные сведения об и нфрастру ктуре ASP.NET Co re MVC Таблица 15. 7 . Сопоставление URL с ограничением Пример URL / /Home/CustomVariaЫe/ H ello /Home/CustomVariaЫe/l /Home/CustomVariaЬle/1/2 На что отображается controller=Home action=I ndex id=null Соответствия нет - сегмент id не может быть преобразован в значение int cont roller=Home action=CustomVariaЫe id=l Соответствия нет - сегм ентов слишком много Ограничения также могут быть указаны за пределами шаблона URL с использо­ ванием аргум е нта constra in ts метода MapRoute () при определ е нии м а ршрута. Такой прием удобен , если вы предпочитаете удержи вать шаблон URL отдел ьно от ог­ раничений или следовать стилю маршрутизации, принятому в бол ее ранних версиях MVC , где встроенны е ограничения не поддерживались. В листинге 15.26 показано то же самое ограничение int на переменной сегмента id, выраженное с применением отдельного ограничения. При использовании такого формата стандартны е з нач е ния также выражаются вн ешне. Листинг 15.26. Выражение ограничения за пределами шаблона URL в файле Startup. cs u sing Microsof t. AspNetCore . Bu i l der ; using Microsoft . Extensions . Dependencyinjection ; using Microsoft.AspNetCore.Routing.Constraints; n arne space UrlsAndRoutes puЫic class Startup { puЬl i c void ConfigureServices(IServi ceCollection services) { services . AddMvc() ; pu Ы i c void Configure(IAppl icat ionBuilder арр) { app .Us eStatusCodePa g es() ; app . UseDeveloperExceptionPage() ; app . UseStaticFi les() ; app .U seMvc(routes => { routes . MapRo ute(name : " MyRoute ", )); template: "{controller}/{action}/{id?}", defaults: new { controller = "Ноте", action = "Index" } , constraints: new { id = new IntRouteConstraint() }) ; Аргумент constrain ts метода Ma pRo ut e ( ) определен с прим енени ем анонимного типа, им ена свойств которого соотв етствуют ограничив аемым пер еменны м сегм ен­ тов. Прост ранс тво имен Mic r osoft . AspNetCo r e . Rout i ng . Constraints сод е ржит набор классов , которые можно использовать для определения индивидуальных огра­ ничений. В листинге 15.26 аргумент con s trai n ts нас т роен на при м енени е объ е кта I ntRoute Const r aint для сегмента id, что дает такой же результат. как встро енное ограничение из листинга 15.25.
Глава 15. Маршрутизация URL 453 В табл. 15.8 описан полный набор классов ограничений в пространстве им ен Microsoft .AspNetCore . Routing . Constraints вместе с их встро енными э квива­ лентами для огранич ений, которые могут прим еняться к одиночным элементам в шаблон е URL; ч асть из них будет р ассматрив аться в последующих разделах . Совет. Можно ограничить доступ к методам действий со стороны запросов, которые сдела­ ны с помощью специфических НТТР-команд, таких как GE T или POST , используя набор атрибутов MVC наподобие HttpGet и HttpPost. В главе 17 описано применение этих атрибутов для обработ ки форм в контроллерах , а в главе 20 приведен полный список до­ ступных атрибутов. Таблица 15.8. Ограничени я маршрутов на уровне сегментов Встроенное ограничение alpha bool datetirne decirna l douЫe float guid int length (l en) length (min , max) long Описание Соответствует алфавитным символам независимо от ре- гистра (A-Z , a-z) Имя класса Al phaRouteConstraint() Соответствует значению, BoolRouteConstraint () которое может быть преобра- зовано в тип boo l Соответствует значению, DateTi rneRouteConstrain t () которое мо жет быть преобра- зовано в тип DateTirne Соответствует значению, DecirnalRouteConstraint () которое может быть преобра- зовано в тип decirnal Соответствует значению, DouЬleRouteConstraint () которое может быть преобра- зовано в тип douЫe Соответствует значению, Fl oatRouteCons traint () которое может быть преобра - зовано в тип float Соответствует значению GuidRouteConst r aint () глобально уникального идентификатора Соответствует значению, IntRouteConstr aint () которое может быть преобра- зовано в тип int Соответствует строке , кото- Le n gth RouteConstraint ( len) рая содержит у казанное ко - личество символов или число LengthRouteConstraint (rnin , max) символов между rnin и max (включительно) Соответствует значению, LongRouteConstraint () которое может быть преобра- зовано в тип long
454 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC Встроенное ограничение Описание Имя класса Окончание табл . 15.8 maxlength(len) Соответствует строке с количе- MaxLengthRouteConstraint (len) ством символов не более len max(val) minlength(len) min(val) range (min , max) regex(expr) Соответствует значению int, MaxRouteConstraint (val) если оно меньше val Соответствует строке, кото- MinLengthRouteConstraint (len) рая имеет, по крайней мере, len символов Соответствует значению int, MinRouteConstraint (val) если оно больше val Соответствует значению int, RangeRouteConstraint (min , max) если оно находится между min и max (включительно) Соответствует регулярному RegexRouteConstraint (expr) выра жению Ограничение маршрута с использованием регулярного выражения Наибольшую гибкость предлагает огранич ение regex, которое сопоставляет сег­ мент с применением регулярного выражения. В листинге 15.27 сегмент controller ограничивается диапазоном URL. с которыми он будет сопоставляться. Листинг 15.27. Использование регулярного выражения для ограничения маршрута в файле Startup. cs using Microsoft.AspNetCore . Builder ; using Mi crosoft . Extensions . Dependencyinjection ; using Microsoft . AspN etCo r e . Routing . Constraints ; nam espace UrlsAndRoutes puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services . AddMvc(); puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeve loperExceptionPage() ; app.UseStaticFiles(); app .U seMvc(routes = > { routes . MapRoute ( name : " MyRoute", template: "{controller:regex("H .* )=Home}/{action=Index}/{id?}"); });
Глава 15. Маршрутизация URL 455 Примененное к маршруту ограничение обеспечивает его соответствие только та­ ким URL, в которых сегмент controller начинается с буквы Н. На заметку! Стандартные значения используются перед проверкой ограничений. Таким об­ разом, например, при запросе URL вида / для переменной сегмента controller при­ меняется стандартное значение Ноте. Затем проверяются ограничения , и поскольку зна­ чение controller начинается с Н, стандартный URL будет соответствовать маршруту. С помощью регулярных выражений можно ограничивать маршрут так, что к совпадению будут приводить только специфические значения для какого-то сегмен­ та URL. Это делается с использованием символа 1, как показано в листинге 15.28. (Шаблон URL разбит на две части, чтобы уместиться на печатной странице, что в реальном проекте совершенно необязательно . ) Листинг 15.28. Ограничение маршрута специфическим набором значений переменной сегмента в файле Startup. cs using Microsoft .AspNetCore.Builder ; using Microsoft.Extensions.Dependencyinjection; using Microsoft . AspNetCore.Routing . Constraints ; namespace UrlsAndRoutes { puЫic class Startup ( puЫic void ConfigureServices(IServiceCollection services) { services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) ( app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app . UseMvc(routes => ( routes.MapRoute(name: "MyR oute ", template: "{controller:regex(лH.*)=Home}/" + "{action:regex(лindex$1"AЬout$)=Index}/{id?}"); }); Такое ограничение разрешает маршруту соответствовать только URL, имеющим сегмент action со значением Index или About . Ограничения применяются совмес­ тно, так что ограничения, наложенные на значение переменной action, комбиниру­ ются с ограничениями, наложенными на значение переменной controller. Другими словами, маршрут в листинге 15.28 будет соответствовать только таким URL, в кото­ рых значение переменной controller начинается с буквы Н, а значением перемен­ ной action является Index или AЬout . Использование ограничений на основе типов и значений Большинство ограничений применяются для ограничения маршрутов, так что они соответствуют только URL с сегментами, значения которых могут быть преобразова­ ны в указанный тип либо имеют специфический формат. Хорошим примером может
456 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC служить ограничение int, использованное в начале этого раздела: маршруты будут соответствовать, только 1шгда значение ограниченного сегмента может быть преоб­ разовано в значение .NЕТ-типа int. В листинге 15.29 демонстрируется при менен ие ограничения range, 1шторое ограничивает маршрут так. что он соответствует URL, только когда значение сегмента может быть преобразовано в int и находится между указанными значениями. Листинг 15.29. Ограничение сегмента на основе типа и значения в файле Startup. cs using Microsoft . AspNetCore.B u ilder ; using Mic r osoft .Extensions . Dependencyinjection ; using Microsoft.AspNetCore.Routing.Constraints; namespace UrlsAndRoutes { puЬlic class Startup { puЫic void Configu re Services( I Se rvi ceCollect i on services) { servic es . AddMvc() ; puЫic void Configure(IApp licationBuilder арр) { app .UseS tatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app .U seMvc(routes => { routes . MapRoute(name : "M yRoute ", template: "{controller=Home}/{action=Index}/{id:range(l0,20)?}"); }); Ограничение в рассмотренном примере применялось к необязательному сегменту id. Ограничение будет проигнорировано, если URL запроса не содержит, по крайней мере, три сегмента. Если сегмент id присутствует, то маршрут будет соответствовать URL, только когда значение сегмента может быть преобразовано в int и попадает в диапазон между 10 и 20. Ограничение range является включающим, т.е. значения 10 и 20 считаются находящимися внутри диапазона. Объединение ограничений Если необходимо применить 1t одиночному элементу сразу несколыш ограничений, тогда их можно соединить в цепочку, отделяя каждое ограничение от других двоето­ чием (листинг 15.30). Листинг 15.30 . Объединение встроенных ограничений в файле Startup. cs using Mi crosoft . AspNetCore . Bui lder ; using Microsoft .Extensions .D epe n dency inj ect i on ; using Microsoft .AspNetCore .Routing . Constraints ; namespace Ur l sAndRoutes { puЬlic class Startup { puЫic void ConfigureServices(ISe r viceCollection services) { servi ces.AddMvc() ;
Глава 15 . Маршрутизация URL 457 puЬlic void Configure(IApplicationBuilder арр) { app .U seStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app . UseMvc(routes => { routes . MapRoute(name: " MyRoute ", }); template: "{controller=Home}/{action=Index}" + "/{id:alpha:minlength(б)?}"); В этом листинге к сегменту id бьши применены ограничения alpha и minlength. Знак вопроса, обозначающий необязательный сегмент, применяется после всех ог­ раничений. В результате объединения ограничений маршрут будет соответствовать URL. только когда сегмент id опущен (поскольку он необязателен) или когда он при­ сутствует и содержит, по крайней мере, шесть алфавитных символов. Если вы не используете встроенные ограничения, то должны применять класс Microsoft . AspNetCore . Routing . Composi teRouteConstraint, который позволя­ ет ассоциировать несколько ограничений с одиночным свойством в анонимно типи­ зированном объекте. В листинге 15.31 показана комбинация ограничений, которые использовались в листинге 15.30 . Листинг 15.31. Объединение отдельных ограничений в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft .E xtensions .Dependencyinjection ; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles(); app . UseMvc(routes => { routes . MapRoute(name : " MyRoute ", )); template: "{controller}/{action}/{id?}", defaults: new { controller = "Ноте", action = "Index" } , constraints: new { id = new CompositeRouteConstraint( new IRouteConstraint [] { }); new AlphaRouteConstraint(), new MinLengthRouteConstraint(б) })
458 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Конструктор класса Composit e Rou teCo n st r a i nt принимает перечисление объ­ ектов, реализующих интерфейс IRouteCon st ra int, которое определяет ограничения маршрутов. Система маршрутизации разрешит соответствие маршрут а URL, только если все ограничения удовлетворены . Определение специального ограничения Если для обеспечения текущих потребностей стандартных ограничений оказыва­ ется недостаточно, тогда можно определить собственные ограничения, реализовав инте рфейс IRout e Con s tra i nt , который находится в пространстве и ме н Mi crosoft . A s pNetCo r e . Rout i ng . Чтобы проверить такую возможность , добавьте в пример проекта папку Inf ra s tr u ct ure и поместите в нее новый файл класса по имени Week DayConst r a i nt . c s с содержимым из листинга 15 .32. Листинг 15.32. Содержимое файла WeekDaycon strain t . c s из папки Infrastructure using Microsoft . As pNetCore . Htt p ; u sing Mi c r osoft . AspNetCore . Routing; using System.Linq; n amespace UrlsAndRo u tes . Infra s tructure puЫic clas s We ekDayConstra i nt : I RouteConstra int private static string[] Days = new[] { "mon" , "tue", "wed", "thu", "fri", "sat", "sun" }; puЫ ic bool Match(HttpCont ext httpContext , IRouter route , string rou t eKey , RouteVal u eDic tionary v a lues , Route Direction routeDirect i on) { retu r n Days . Contains{val ues[route Key]? . ToString() . ToLowerinvariant()) ; В интерфей се IRo u teCon s tr ain t определен метод Ma tch () , который вызывает­ ся , чтобы позволить ограничению решить, должен ли запрос соответствовать марш­ руту . Парамет ры метода Match () пр едоставляют доступ к запросу , по ступивш е му от клиента , маршруту, имени сегм ента, на который воздействует ограничение, перемен­ ным сегментов, извлеченным из URL, и признаку того, для какого URL проверяется запрос - входящего или исходящего (исходящие URL рассматриваются в главе 16) . В этом прим е ре с помощью параметра routeKey из парам етра values и з вл е ка ет­ ся значение переменной сегмента, к которому было применено ограничение, преобра ­ зуется в строку нижнего регистра и проверяется на предмет соответствия одному из дней недели, определенных в статическом пол е Days . В листинге 15.33 новое ограни­ чение применяется к маршруту с использованием синтаксиса отдельно определяемых ограничений. Листинг 15.33 . Применение специального ограничения в файле Startup . cs using Microsoft . AspNetCore . Bu ilder ; u sing Microsoft . Ext e nsions . Dependency i njection ; usi ng Mi crosoft. AspN etCore . Routing . Co n str aints ; usi ng Mi crosoft.AspNet Core.Routing; using UrlsAndRo utes.Infrastructure; namespac e Ur lsAndRoutes {
Глава 15. Маршрутизация URL 459 puЫic class Startup { puЫic void Config ureS e r vices( I ServiceCollecti on services) { services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app .U seStaticFi les(); app . UseMvc(routes => { routes . MapRoute(name : " MyRoute ", )); template: " {controller}/{action} /{id?} ", defaults : new { controller = "Н оте ", action = "Index" } , constraints: new { id = new WeekDayConstraint() }) ; Этот маршрут будет соответствовать URL, только если сегмент id отсутствует (например, /Customer/List) или совпадает с одним из дней недели, определенных в ш~ассе ограничения (скажем, /Customer/List/Fri). Определение встроенного специального ограничения Настройка специального ограничения так, чтобы оно могло использовать­ ся встроенным образом, требует дополнительного шага по конфигурированию (листинг 15 .34). Листинг 15.34. Применение специального ограничения встроенным образом в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions.Dependency!njection; using Microsoft . AspNetCore.Routing . Co ns traints ; using Microsoft.AspNe tC ore . Routing; using Ur l sAndRoutes . Infrastructure; namespace UrlsAndRoutes puЫic class Startup { puЬlic void ConfigureServi ces(IServiceCol l ection services) { services.Configure<RouteOptions>(options => options.Constraintмap.Add("weekday", typeof(WeekDayConstraint))); services.AddMvc(); puЬlic void Conf ig ure( I ApplicationBuilder арр) { app . UseStatusCode Pages() ; app .UseDevel op e rExceptionPage() ; app.UseStaticFiles() ; app.UseMvc(routes => { routes . MapRoute(name : " MyRoute ", template: "{controller=Home}/{action=Index}/{id:weekday?}"); ));
460 Часть 11 . Подробные сведения об инфраструктуре ASP.NE T Саге MVC В методе ConfigureService () конфигурируется объект RouteOptions, который управляет рядом линий поведения системы маршрутизации . Свойство ConstraintMap возвращает словарь, используемый для трансляции имен встроенных ограничений в классы реализации IRouteConstraint, которые предоставляют логику ограничений . В словарь было добавлено новое отображение, поэтому на класс WeekDayConstraint можно ссылаться встроенным образом как на weekday: template : " {controller= Home}/{act ion=Inde x }/{ id:weekday?} ", Результат будет тем же самым, но настройка отображения позволяет прим енять специальный класс ограничения встроенным образом. Использование маршрутизации с помощью атрибутов Во всех примерах, приведенных до сих пор в главе , маршруты определялись пос­ редством приема, который называется маршрутизач,ией на основе соглашений. Инфраструктура MVC также поддерживает прием, известный как маршрутизач,ия с помощью атрибутов, когда маршруты определяются через атрибуты С# , которые применяются напрямую к классам контроллеров. В последующих разделах будет по­ казано, как создавать и конфигурировать маршруты с использованием атрибутов. которые могут свободно смешиваться с маршрутами на основ е соглаш ений, проде­ монстрированными в пр едшествующих примерах . Подготовка для маршрутизации с помощью атрибутов Маршрути зация с помощью атрибутов включается, когда в файле Startup . cs вызывается м етод UseMvc () . Инфраструктура MVC исследует классы контроллеров в приложении, находит любые из них , имеющие атрибуты маршрутизации, и создает нужные маршруты. В этом раздел е мы возвратим пример приложения к стандартной конфигурации маршрутизации, описанной во врезке " Стандартная конфигурация маршрутизации" ранее в главе, как показано в листинге 15.35 . Листинг 15.35 . Применение стандартной конфигурации в файле Startup. cs using Microsoft . AspNetCore.Builder; using Microsoft . Extensions.Dependencylnjection; u si ng Microsoft . AspNetCore.Routing.Constraints ; using Microsoft . AspNetCore . Routing ; using UrlsAndRoutes . Infrastructure ; namespace Ur l sAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollect ion services) { services . Configure<RouteOptions>(options => options.ConstraintMap .Add( " weekday ", typeof(WeekDayConstraint) )) ; services . AddMvc() ; puЬlic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app .U seStati cFil es() ;
Глава 15. Маршрутизация URL 461 app.UseMvcWithDefaultRoute(); Стандартный маршрут будет сопоставляться с URL, используя следующий шаблон: {cont r oller}/{ac t ion }/ {id?} Применение маршрутизации с помощью атрибутов Атрибут Rou te используется при указании маршрутов для индивидуальных контроллеров и действий. В листинге 15.36 атрибут Route применяется к классу Customer Con t roller . Листинг 15.36 . Применение атрибута Route в файле CustomerController.cs using Mi crosoft .As pNetCore . Mvc ; us i ng UrlsAndRoutes. Mode ls; namespace OrlsAndRoute s. Con trol l ers puЫic class CustomerController : Controll er [Route("myroute")] puЫic ViewResult Index() => Vi e w( "Res ult", new Result { Controller = nameof(CustomerCo n t r oller) , Action = name of (Index ) }); puЫic ViewResult List(st ring id) { Resultr =new Result { Controller = nameof(Home Control le r) , Action = nameof(List) , }; r.Data["id"] =id ?? "<no value>"; r .Data["cat chall"] = RouteDat a .Val ues["catchall"]; return View( "Resul t ", r) ; Атрибут Route определяет маршрут к методу действия или контроллеру , к которо­ му он применен. В листинге этот атрибут применен к методу действия I ndex () сука­ занием my r oute в качестве маршрута, который должен использоваться. Результатом является изменение набора маршрутов, которые применяются для достижения мето­ дов действий, определенных контроллером Cu stomer (табл. 15.9). Таблица 15.9. Маршруты для контроллера Customer Маршрут /Customer/List /myro ute Описание Этот URL нацелен на метод действия Lis t () , полагаясь на стандартный маршрут в файле Star tup . c s Этот URL нацелен на метод действия Index ( )
462 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Следует отметить два важных момента. Во-первых, когда используется атрибут Route, предоставленное для его конфигурирования значение применяется при оп­ ределении полного маршрута, так что myrou te становится завершенным URL для достижения метода действия Index (). Во-вторых, присутствие атрибута Route предотвращает использование стандартной конфигурации маршрутизации, поэто­ му метод действия Index () больше не будет достижимым с применением URL вида /Customer/Index. Изменение имени метода действия Определение уникального маршрута для одиночного метода действия в большинс­ тве приложений не особенно полезно, но атрибут Route можно использовать более гибким образом. В листинге 15.37 внутри маршрута применяется специальный мар­ кер [controller] для ссылки на контроллер и настройки базовой части маршрута. Совет. Изменить имя действия можно также с помощью атрибута ActionName, который рассматривается в главе 31 . Листинг 15.37. Использование атрибута Route для переименования действия в файле CustomerController. cs using Microsoft . AspNetCore . Mvc; using UrlsAndRoutes . Models ; narnespace UrlsAndRoutes . Controllers puЫic class CustornerController : Controller [Route("[controller]/MyAction")] puЫic ViewResult Index() => View( " Resu lt", new Result { Controller = narneof(CustornerController) , Action = narneof(Index) }); puЫic ViewResult List(string id) { Result r = new Result { Controller = narneof(HorneController), Action narneof(List), ); r.Data["id"] = id ?? "<no value> "; r.Data["catchall"J = RouteData .Values[ " catchall "J; return View( " Result ", r); Применение маркера [c ontro ller] в аргументе для атрибута Route довольно похоже на использование операции nameof и позволяет указывать маршрут к кон­ троллеру без жесткого кодирования имени класса. В табл. 15.10 описан эффект от наличия атрибута в листинге 15.37. Таблица 15. 1О. Маршруты для контроллера Customer Маршрут /Customer/List /Customer/MyAction Описание Этот URL нацелен на метод действия List () Этот URL нацелен на метод действия Index ()
Глава 15 . Маршрутизация URL 463 Создание более сложного маршрута Атрибут Rout e можно также применять к классу контроллера, позволяя опреде­ лять структуру маршрута, как показано в листинге 15.38 . Листинг 15.38 . Применение атрибута Route к контроллеру в файле CustomerController.cs using Microsoft .A spNetCo r e . Mvc ; using OrlsAndRoutes . Model s; n amespace Url s AndRoutes . Con trol l ers [Route("app/[controller]/actions/[action]/{id?}")] puЬlic class CustomerControl l er : Controller { puЫic ViewResult Index() => View( " Re s u l t ", new Resu lt { Controller = nameof( Cus t omerCo n tro ller) , Action }); puЫic ViewResult List (string i d) { Resultr =new Result { nameo f (Index ) Controller = nameof(HomeControlle r ) , Action nameof(Lis t) , }; r .Data["id"J = id ?? "<по value>"; r. Data[ " catchall "J = Ro uteData . Values [" catch al l" J ; return View("Result", r ); В маршруте определ ена смесь статических и переменных сегментов, а также ис­ пользуются маркеры [cont r o ller] и [act i o n] для ссылки на имена класса контрол­ лера и методов действий. В табл. 15.11 описан эффект от создания этого маршрута. Применение ограничений к маршрутам Маршруты, определенные посредством атрибутов , могут ограничиваться подобно маршрутам, которые определены в файле Startup . cs , с помощью того же самого встроенного подхода, используемого для маршрутов на основе соглашений . В листин­ ге 15.39 созданное ран ее в главе специальное ограничение применяется к необяза­ тельному сегменту id, определенному в атрибуте Route . Таблица 15.11 . Маршруты для контроллера Cus torner Маршрут app/customer/acti o n s/index app/custome r/act i ons/ i n dex /myid app/customer/actions/li st app/customer/actions/li s t /myid Описание Этот URL нацелен на метод действия Ind e x () Этот URL нацелен на метод действия I ndex ( ) с необязательным сегментом i d, установлен­ ным в myid Этот URL нацелен на метод действия List () Этот URL нацелен на метод действия Li st () с необязательным сегментом id, установлен­ ным вmyi d
464 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 15.39 . Ограничение маршрута в файле CustomerController. cs using Microsoft.AspNetCore . Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRout es .Controllers [Route("app/[controller]/actions/[action]/{id:weekday?}")] puЬlic class CustomerController : Controller { puЫic ViewResult Index() => View("Result" , new Result { Controller = nameof(CustomerController), Act ion = nameof (Index) )); puЬlic ViewResult List(string id) { Result r = new Result { Controller = name of( HomeController), Action = nameof(List ) , }; r . Data["id"] = id ?? "<no value>" ; r.Data["catchall"J = RouteData.Values["catchall"J; return View( " Result ", r); Можно использовать все ограничения, описанные в табл. 15.8 , или, как проде­ монстрировано в листинге 15.39, применять специальные ограничения, которые были зарегистрированы с помощью службы RouteOptions. Чтобы применить сразу несколько ограничений, их понадобится соединить в цепочку, отделяя друг от друга двоеточиями. Резюме В этой главе подробно обсуждалась система маршрутизации. Вы увидели, как оп­ ределяются маршруты по соглашениям и с помощью атрибутов, каким образом сопос­ тавляются и обрабатьmаются входящие URL, и как настраиваются маршруты за счет изменения способа, которым они сопоставляются с сегментами URL, а также за счет использования стандартных значений и необязательных сегментов. Кроме того, бьmо показано, каким образом ограничивать маршруты, чтобы сузить диапазон запросов, для которых они будут давать совпадение, с применением встроенных ограничений и специальных классов ограничений. В следующей главе мы рассмотрим генерацию исходящих URL на основе маршру­ тов внутри представлений и выясним, как использовать средство областей, которое полагается на систему маршрутизации и может применяться для управления круп­ ньrми и сложными приложениями МVС.
ГЛАВА 16 Дополнительные возможности маршрутизации в предыдущей главе было показано, как использовать систему маршрутизации для обработки входящих URL, но это только часть общей картины. Мы также должны быть в состоянии применять схему URL для генерации исходящих URL, ко­ торые могут встраиваться в представления, чтобы пользователи имели возможность щелкать на ссылках и отправлять формы обратно приложению, нацеливаясь на кор­ ректный контроллер и действие. В этой главе мы рассмотрим различные приемы для генерации исходящих URL , продемонстрируем способы настройки системы маршрутизации за счет замены стан­ дартных классов, реализующих маршрутизацию МVС, и объясним, как использовать средство областей MVC, которое позволяет разбивать крупное и сложное приложение MVC на управляемые фрагменты. IЛава завершится рядом советов по применению схем URL в приложениях MVC. В табл. 16.1 приведена сводка, позволяющая помес­ тить дополнительные возможности маршрутизации в контекст. Таблица 16. 1. Помещение дополнительных возможностей маршрутизации в контекст Вопрос Что это такое? Чем они полезны? Ответ Система маршрутизации предоставляет возможности, которые вы­ ходят за рамки сопоставления с URL для НТТР-запросов. Имеется также поддержка для генерации URL в представлениях, замеще­ ния встроенной функциональности маршрутизации специальными классами и структуризации приложения в виде изолированных разделов Каждое средство полезно по своим сообра ж ениям. Наличие воз­ можности генерации URL облегчает изменение схемы URL без необходимости в обновлении всех представлений . Существование возможности использования специальных классов позволяет под­ страивать систему маршрутизации под собственные требования. Наличие возможности структуризации приложения упрощает пост­ роение сложных проектов Как они используются? Ищите детальные сведения в разделах настоящей главы
466 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Вопрос Существуют ли какие­ то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией MVC 5? Окончание табл. 16. 1 Ответ Конфигурация маршрутизации для сложного приложения может стать трудной в управлении Нет. Система маршрутизации - это неотъемлемая часть ASP.NET Core Помимо базовых изменений, описанны х в главе 15, дополнитель­ ные возможности изменились следующим образом. Специальные классы маршрутизации реализуют интерфейс IRouter, а не являются производными от класса RouteBase, как было в ранни х версия х MVC . Классы, которые реализуют маршрутизацию, теперь ответственны за предоставление делегатов для обработки запросов, чтобы вы­ пускать ответ, а не только выполнять сопоставление с URL . Когда применяются области, предполагается нахождение контрол­ леров в главной части приложения, если только они не были деко­ рированы атрибутом Area, даже когда файл класса создан в папке Contro llers области. В табл . 16.2 приведена сводка для настоящей главы. Таблица 16.2. Сводка по главе Задача Генерац и я элемента а с URL Предоставление значений для сег­ ментов маршрутизации Генерация полностью заданны х URL Выбор маршрута для генерации URL Генерация URL без НТМL-элемента Решение Используйте атрибуты asp- action и asp - controller Применяйте атрибуты с префи ксом asp- route - Используйте атрибуты asp- procotol , asp- host и asp-f ragment Применяйте атрибут asp - route Используйте вспомогательный метод Ur1 . Action () в представлении или в методе действия Настройка системы маршрутизации Применяйте метод Configure () в классе Startup Создание специального класса Реализуйте интерфейс IRouter маршрутизации Разбиение приложения на функцио- Создайте области и используйте нальные разделы атрибут Area Листинг 16.1-16.5 16.6-16.7 16.8 16 .9-16.10 16 .11 - 16.12 16.13 16.1 4 -16.21 16.22-16.28
Глава 16 . Дополнительные возмо ж ности маршрутизации 467 Подготовка проекта для примера Мы продолжим работать с проектом UrlsAndRoutes из предыдущей главы. Единственное изменение потребуется внести в клас с Startup, где вызов метода UseMvcWi thDefaul tRoute () заменяется явным маршрутом с тем же самым резуль­ татом, как показано в листинге 16.1 . Совет. Если вы не хотите воссоздавать примеры вручную , тогда загрузите готовые проекты Visual Studio из веб-сайта издательства. Листинг 16.1 . Изменение конфигурации маршрутизации в файле Startup. cs using Microsoft . AspNetCore .Builder ; using Microsoft . Extensions . Dependencyinjection ; using Microsoft . AspNetCore.Routing.Constraints ; using Microsoft . AspNetCore . Routing ; using UrlsAndRoutes.Infrastructure ; namespa c e UrlsAndRoutes { puЬlic class Startup { puЫi c void ConfigureServices(IServi ceCol l ection services) { services . Configure<RouteOptions>(options => options . ConstraintMap.Add( " weekday ", typeof(WeekDayConstraint)) ) ; services . AddMvc() ; puЫic void Configure(IApplicationBui lder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles(); app.UseMvc(routes => routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); После запуска приложения браузер запросит стандартный URL, который будет на­ правлен на действие Index контроллера Home (рис. 16 . 1) . D Routing )( --~---°--[~;.~~-~~~s-~~~o;~a~:.==~-=-===~·-===- ~]---~! Controller: HomeCont roller 1 Action: lndex 1 _ ________________________ ) Рис. 16. 1. Запуск примера приложения
468 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Генерация исходящих URL в представлениях Почти в каждом приложении МVС пользователи должны располагать возможностью перехода от одного представления к другому, что обычно предполагает включение в пер­ вое представление ссылки , нацеленной на метод действия, который генерирует второе представление. Бьmо бы заманчиво просто добавить статический элемент а (известный как якорный элемент), указав в его атрибуте href нужный метод действия, например: <а href="/Home/CustomVariaЬle">This is an outgoing UR L</a> Предполагая, что в приложении применяется стандартная конфигурация марш­ рутизации, такой НТМL-элемент создает ссылку, которая будет нацелена на метод действия CustomVariaЬle () в контроллере Home. Вручную определяемые URL вроде этого создавать быстро и просто. Но вместе с тем они чрезвычайно опасны: изменив схему URL для приложения, вы нарушите работу всех жестко закодированных URL. После этого придется пройтись по всем представлениям в приложении и обновить все ссылки на контроллеры и методы действий -утомительный. чреватый ошибка­ ми и сложный в тестировании процесс. Более удачная альтернатива предусматривает использование для генерации исходящих URL системы маршрутизации, что обеспе­ чивает применение схемы URL для динамического формирования URL способом, ко­ торый гарантирует учет схемы URL приложения. Генерирование исходящих ссылок Сгенерировать исходящий URL в представлении проще всего с использованием дескрипторного вспомогательного класса для якоря, который создаст атрибут href в НТМL-элементе а , как иллюстрируется в листинге 16.2, в котором приведено добав­ ление к представлению /Views/Shared/Result . cshtml. Совет. Работа дескрипторных вспомогательных классов объясняется в главе 23. Листинг 16.2. Использование дескрипторного вспомогательного класса для якоря в файле Resul t. csh tml @model Result @( Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Routing</title> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> </head> <body class="panel-body"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЫe-condensed"> <tr><th>Controller :< /th><td>@Model . Controller</td></tr> <tr><th>Act ion: </th><td>@Model.Action</td></tr> @foreach (string key in Model.Data.Keys) ( <tr><th>@key : </th><td>@Model.Data [key] </td></tr> </tаЫе> <а asp-action="CustomVariaЬle">This is an outgoing URL</a> </body> </html>
Глава 16. Доnолнительные возмо жности маршрутизации 469 Атрибут asp-action применяется для указания имени метода действия, на кото­ рый должен быть нацелен URL в атрибуте href. Запустив приложение , можно уви­ деть результат (рис. 16.2). [j Rou tin9 Х 1 - ~ с [? -~;;;ih-;!:60588 1··--- ---- ·---- --- -- -· " · --- " 1 Controller: Action: This is an outgoing URL HomeController lnd ex ~---------------------------- Рис. 16.2 . Использование дескрипторного вспомогательного класса для генерации ссылки Дескрипторный вспомогательный :класс устанавливает атрибут href элемента а с применением текущей конфигурации маршрутизации. Просмотрев НТМL-разметку, отправленную браузеру , вы заметите , что она содержит следующий элемент: <а href=" /Home/CustomVariaЫe" >T hi s is an outgoing URL</a> Может показаться , что для воссоздания вручную определенного URL , который был приведен ранее , пришлось затратить слишком много дополнительных усилий, но пре­ имущество данного подхода в том, что он автоматически реагирует на изменения в конфигурации маршрутизации . Чтобы продемонстрировать сказанное , добавим в файл Startup. cs новый маршрут (листинг 16.3). Листинг 16.3. Добавление нового маршрута в файле Startup. cs using Microsoft . AspNetCore.Bu ilde r; using Microsoft . Extens i ons . Dependencyinjection; using Microsoft.AspNetCore . Routing . Constra i n ts; using Mi crosoft . AspNetCore . Routing ; using UrlsAndRoutes . Infrastructure ; namespace UrlsAndRoutes puЫic class Startup { puЬlic void ConfigureServices(IServiceCol le ction services) { services . Configure<RouteOptions>(op tions => options . ConstraintMap . Add( " wee k day ", typeo f(WeekDayCon straint) ) ); services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvc(routes => {
470 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC routes.MapRoute( name: "NewRoute", template: "App/Do{action}", defaults: new { controller ="Ноте" }) ; routes.MapRoute ( name: "default", template: "{controller= Home)/{action= Index)/{id?)"); }); Новый маршрут изменяет схему URL для запросов, которые нацелены на контрол­ лер Ho me. Запустив приложение, вы увидите, что это изменение отразилось в НТМL­ разметке, сгенерированной дескрипторным вспомогательным классом: <а href="/App/DoCustomVariaЫe" > This is an o utgoing URL </a> Генерирование ссылок с использованием дескрипторного вспомогательного клас­ са решает важную проблему сопровождения. Мы имеем возможность изменять схему маршрутизации, а исходящие ссьmки в представлениях автоматически отразят изме­ нения без необходимости вручную редактировать представления в приложении. Когда производится щелчок на ссьшке, исходящий URL применяется для создания входящего НТГР-запроса, и тот же самый маршрут затем используется для нацелива­ ния на метод действия и контроллер, которые будут обрабатывать запрос (рис. 16.3). [J Routing Х ~ С 1· Q ~~lh~st:WS Sffl.p;/DoC~s;o~Vи11aЫ~- . - Controll er: HomeCoпtroller Act ion : CLJstomVarlaЫe ~.p~--...· 1J ...,,. ..... ~ 1 1 1 J Рис. 16.З. Результатом щелчка на ссылке является применение исходящего URL во входящем запросе Сопоставление исходящих URL с маршрутами Вы уже видели, что изменение маршрутов, определяющих схему URL, изменяет способ ге­ нерации исходящих URL. В приложениях обычно определено множество маршрутов, поэто­ му важно понимать, как они выбираются для генерации URL. Система маршрутизации обра­ батывает маршруты в порядке их определения, и каждый маршрут проверяется по очереди на предмет соответствия, что требует удовлетворения следующих трех условий. • Для любой переменной сегмента, определенной в шаблоне URL, должно быть доступно значение. Чтобы найти значения для каждой переменной сегмента, система маршрути­ зации просматривает сначала предоставленные (посредством свойств анонимного типа) значения, затем значения переменных для текущего запроса и, наконец, стандартные значения, определенные в маршруте. (Позже в главе мы еще возвратимся ко второму ис­ точнику значений из числа упомянутых.)
Глава 16. Дополнительные возмо ж ности маршрутизации 471 • Ни одно из значений, предоставленных для переменных сегментов, не должно конфлик­ товать с определенными в маршруте переменными , которые имеют только стандартные значения . Это переменные, для которых были предоставлены стандартные значения, но которые не встречаются в шаблоне URL . Например, в показанном ни же определении мар­ шрута myVar является переменной, имеющей только стандартное значение: route s. MapRoute( " MyRo ut e ", " {control ler}/{action} ", new { myVar = "true" }) ; • Для соответствия такому маршруту значение для переменной myVar либо не должно пре­ доставляться , либо должно совпадать со стандартным значением . • Значения для всех переменных сегментов должны удовлетворять ограничениям маршру­ та. Примеры разных видов ограничений приводились в разделе "Ограничение маршрутов" предыдущей главы . Чтобы было совершенно ясно : система маршрутизации не пытается найти маршрут, который дает наилучшее совпадение . Она находит только первое совпадение и использует данный маршрут для генерации URL ; любые последующие маршруты игнорируются. По этой при­ чине наиболее специфичные маршруты должны определяться первыми. Важно проверить генерацию исходящих URL. Попытка генерации URL , для которого не удается найти соот­ ветствующий маршрут, приведет к созданию ссылки , содержащей пустой атрибут href: <а href='"' >This i s an outgoing URL< / a> Та кая ссылка корректно визуализируется в представлении, но не будет функционировать ожидаемым образом, когда пользователь выполняет на ней щелчо к. Если вы генерируете только URL (как будет показано поз же в главе), тогда результирующим значением будет null , которое визуализируется в виде пустой строки в представлениях. С помощью име­ нованных маршрутов можно получить определенный контроль над сопоставлением маршру­ тов . За более подробными сведениями обратитесь в раздел " Генерирование URL из специ­ фического м аршрута" далее в главе. Направление на другие контроллеры В случае указания атрибута asp - action внутри эл ем ента а дескрипторны й вспо­ могательный класс пр едполагает, что вы хотите установить в качестве цели действие в том же контроллере, который вызвал визуализацию представления. Чтобы создать исходящий URL , который направляет на другой контроллер, можно применить атри­ бут asp- controller (листинг 16.4) . Листинг 16.4 . Направление на другой контроллер в файле Result.cshtml @model Result @( Layout = null; < ! DOCTYPE html> <html> <head> <meta name= " viewport " content=" width=device - width " /> <title>Routing</t i tle> <link rel= " stylesheet " asp - href -i nclude= "l iЬ/bootstrap/dist/css/* . min . css " /> </head> <body class= " panel - body " > <tаЫе class= " taЫe taЬle - bordere d taЬle - striped taЬle - condensed " > <t r ><th>Cont r ol l er : </th>< td>@Mode l. Contro l le r </td></t r > <t r><th>Action : </th><td>@ Model . Action</td></tr>
472 Часть 11 . Подробные сведен ия об инфраструктуре ASP.NET Соге MVC @foreach (string key in Model .Data.Keys) { <tr><th>@key : </th><td>@Model . Data[key]</td></tr> </tаЫе> <а asp-controller="Aclmin" asp-action="Index">This is an outgoing URL</a> </body> </html> После визуализации представления вы увидите, что сгенерирована следующая НТМL-разметка: <а href= "/Admin" >This targets ano the r controller</a> Запрос к URL, 1юторый нацелен на метод действия Index () контроллера Admin, был выражен дескрипторным вспомогательным классом как /Admin. Системе марш­ рутизации известно, что маршрут, определенный в приложении. по умолчанию будет использовать метод действия Index (), позволяя опускать ненужные сегменты. При выяснении, каким образом нацеливаться на заданный метод действия. сис­ тема маршрутизации включает маршруты, которые были определены с применени­ ем атрибута Route. В листинге 16. 5 атрибут asp - controller нацелен на действие I n dex в контроллере Customer, к которому был применен атрибут Route в главе 15. Листинг 16.5 . Направление на действие, декорированное атрибутом Route, в файле Resul t. cshtml @model Result @{ Layout = null; < ! DOCTYPE html> <html> <head> <meta name= " viewport " content= " widt h=device - width " /> <title>Routing</title> <link rel=" stylesheet " asp-href-include= " liЬ/bootstrap/dist/ css/* . min . css " /> </head> <body class= " panel - body " > <tаЫе class= " taЫe taЫe - bordered taЫe-striped taЫe - condensed " > <tr><th>Controller : </th><td>@Model . Controller</td></tr> <tr><th>Action : </th><td>@Model . Action</td></t r > @foreach (string key in Model . Data . Keys) { <tr><th>@key : </th><td>@Model.Data[key ] </td></tr> </tаЫе> <а asp-controller="Customer" asp-action="Index">This is an outgoing URL</a> </body> </html> Вот иакая ссылка сгенерируется: <а href= "/app/Customer/actions/Index" >This is an outgoing URL</a> Результат соответствует атрибуту Route, который применялся к контроллеру Customer в главе 15: [Route("app/[controller]/actions/[action]/{id:weekday? }")] puЫic class CustomerControl l er : Contro l ler {
Глава 16. Дополнительные возмо ж ности маршрутизации 473 Передача дополнительных значений Системе маршрутизации можно передавать значения для переменных сегментов за счет определения атрибутов с именами, начинающимися со строки asp - ro ut e -, за которой следует имя сегмента. Таким образом, атрибут asp - route -id использует­ ся для установки значения сегмента id (листинг 16.6). Листинг 16.6 . Предоставление значений для переменных сегментов в файле Resul t. cshtml @model Result @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name="viewport " content=" width=device - width " /> <tit le >Ro uting</title> <link rel=" stylesheet" asp-href-include=" liЬ/bootstrap/dist/ css/* .rnin. css " /> </head> <body class= " panel - body " > <tаЫе class= " taЫe taЫe-bordered taЫe -st riped ta Ьle-condensed" > <tr><th>Control ler: </ th ><td>@Model .Controller</td></tr> <tr><th>Act ion:< / th><td>@ Mo del.Action</td></ tr> @foreach (string key in Model.Data.Keys) { <tr><th >@key : </th><td>@Model .Data[key]</td></tr> } </tаЫе> <а asp-controller="Horne" asp-action="Index" asp-route-id="Hello"> This is a n outgoing URL </а> </body> </ht rnl> Здесь предоставляется значение для переменной сегмента по имени id. Если при­ ложение применяет маршрут, показанный в листинге 16.3, тогда при визуализации представления будет получена следующая НТМL-разметка : <а href=" /App/Doindex? id=Hello">This is an outgoing URL</ а> Обратите внимание, что значение сегмента было добавлено как часть стро1ш за­ проса, чтобы вписываться в шаблон URL. определенный маршрутом. Причина свя­ зана с отсутствием переменно й сегмента, которая бы соответствовала id в этом маршруте. Чтобы решить проблему, необходимо отредактировать файл Startup. cs, оставив только маршрут, который имеет сегмент id (листинг 16.7). Листинг 16. 7. Редактирование маршрутов в файле Startup. cs using Microsoft .AspN etCore .B uilder ; using Microsoft . Extensions.Dependencyi nje ction ; using Microsoft . AspNetCore.Routing.Constraints ; using Microsoft.AspNetCore.Routing ; using UrlsAndRoutes . Infrastructure; namespace UrlsAndRoutes {
474 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services . Configure<RouteOptions>(options => options.ConstraintMap . Add( " weekday ", typeof(WeekDayConstraint))) ; services.AddMvc(); puЫic void Configure(IApplicationBuilder арр) { app.OseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app.OseStaticFiles(); app . OseMvc( rout es => { // routes.MapRoute( 11 11 11 name: "NewRoute " , template: "App/Do{action}", defaults : new { controller = "Home" } ) ; r oute s.MapRoute( name : " default ", template: " {controller=H ome}/{action =Index}/{ id? } " ) ; }); Запустив приложение еще раз, вы увидите, что дескрипторный вспомогательный класс выдал следующий НТМL-элемент, в котором значение свойства id включено как с егмен т URL: <а href= " /Home/Index/Hello" >This is an outgoing ORL</a> Повторное использование переменных сегментов При описании способа сопоставления маршрутов для исходящих URL объяснялось, что ког­ да система маршрутизации пытается найти значения для каждой переменной сегмента в шаблоне URL маршрута, она просматривает текущий запрос. Такое поведение сбивает с толку многих программистов и может вылиться в длительный сеанс отладки. Предположим, что прило жение имеет единственный маршрут: app .U seMvc(routes => { r outes .MapR oute (name : " MyRoute ", template : " { controller} / {action} / {color} / {page} ") ; }); Пусть в текущий момент пользователь запросил URL вида /Home/Index/Red/100 и ви­ зуализируется показанная ниже ссылка: <а asp - controller= " Home" asp -a ction= "Index" asp -route - page="789 " > This is an outgoing ORL </а>
Глава 16. Дополнительные возмо ж ности маршрутизации 475 Можно было бы ожидать , что система маршрутизации не сумеет выполнить сопоставление с маршруто м, т. к . не было предоставлено значение для переменной сегмента colo r , к тому же для нее не определено стандартное значение . Однако это не так. Система маршрути ­ зации будет сопоставлять с определенным ранее маршрутом. В результате сгенерируется следующая НТМL-разметка: <а href = " / Home/Index/Red/789" >This i s a n outgoi ng URL</a> Система маршрутизации достаточно интеллектуальна, чтобы провести сопоставление с маршрутом, поскольку при генерировании исходящего URL она будет повторно использо­ вать значения переменных сегментов из входящего URL . В этом случае переменная color получит значение Red, т. к. его включал URL, с которого стартовал пользователь. Это не поведение, предназначенное для крайних случаев . Система маршрутизации будет применять такой прием как часть обычной оценки маршрутов , даже если имеется последу­ ющий маршрут, который обеспечит совпадение, не требуя повторного использования зна­ чений из текущего запроса. Я настоятельно рекомендую не полагаться на описанное поведение и предоставлять значе­ ния для всех переменных сегментов в шаблоне URL. Если опираться на данное поведение, то код станет гораздо труднее для восприятия, к тому же в коде будут делаться допущения относительно порядка, в котором пользователи выдают запросы, что обернется проблема­ ми при сопровождении прило жения . Генерирование полностью заданных URL Все ссылки, которые генерировались до сих пор, содержали относительные URL, но дескрипторный вспомогательный иласс для якорного элемента способен также ге­ нерировать полностью заданные URL, как показано в листинг е 16.8. Листинг 16.8 . Генерирование полностью заданного URL в файле Resul t. cshtml @model Resu l t @{ Layout = null ; < ! DOCTYPE html> <html > <h ead> <meta name= " viewpo rt" cont ent = " wi dth=device-widt h " /> <title>Rout in g< /title> <link rel= " style sheet" asp - href - inc lude ="liЬ/b ootstra p /d i st/c s s /*.min. css " /> </head> <body class= " pane l- body " > <tаЫе class= " taЫe ta Ы e - bordered t aЫe - striped taЬle - condensed " > <tr><th>Con tro l ler : </th><td>@Model . Controller</t d></t r > <tr><th>Action : </th><td>@Model . Ac tion</ t d></tr> @foreach (string key in Model . Da t a .Keys) { <tr><th>@key : </th><td>@Mode l. Dat a[ key ] </td></tr> } </tаЫе> <а asp-controller="Home" asp-action="Index" asp-route-id="Hello" asp-protocol="https" asp-host="myserver.mydomain.com" asp-fragment="myFragment">This is an outgoing URL </а> </body> </html>
476 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Атрибуты asp -p ro t ocol , asp - ho s t и asp-fra gm e nt применяются для указания протокола (ht t p s в листинге) , имени сервера (my s erver . my doma in. com) и фрагмен­ та URL (myFragme n t ) . Эти значения объединяются с выводом из системы маршрути­ зации для создания полностью заданного URL, который вы можете увидеть , запустив приложение и просмотрев отправленную браузеру НТМL-разм етку: <а href= " https : //myse r ver . mydomain . com/Home/Index/Hello#myFragment " > This is an outgoi ng URL </а> Будьте осторожны при использовании полностью заданных URL , поскольку они со­ здают зависимости от инфраструктуры прилож ения и. когда инфраструктура изменя­ ется, вы должны не забыть о внесении соотв етствующих измен ений в представления мvс. Генерирование URL из специфического маршрута В предшествующих прим ерах система маршрутизации выбирала маршрут, кото­ рый будет применяться для ген ерации URL. Если важно генерировать URL в специ­ фическом формате, тогда можно указать маршрут, подлежащий использованию для генерации исходящего URL. Чтобы продемонстрировать это в работе, добавьте в файл Startup . c s новый маршрут, обеспечив наличие в примере приложения двух м а рш­ рутов (листинг 16.9). Листинг 16.9. Добавление маршрута в файле Startup. cs using Mi cr osoft.AspNetCore .Builder; u s ing Microsoft . Extensi ons . Dependency i n jecti on ; using Microsoft. AspNetCore .Routing.Constraint s ; us ing Microsoft . AspNet Core . Ro ut ing ; us ing UrlsAndRoutes .Infr astructure; namespace UrlsAndRoutes puЫic cl ass Startup { puЫic void ConfigureServices(ISe rviceCol l ection services) { services . Conf i gure<RouteOptions>(options => opt i ons . Co n strai n tMap . Ad d( " weekday ", typ eof( WeekDayConstraint))) ; services .AddMvc() ; puЫ ic void Configur e(IAppli cati onB uilder арр) { app .U seStat u sCode Pages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFile s () ; app . UseMvc(routes => { routes . MapRoute( name : " default ", t e mplate : " {controller= Home)/{action=Index}/{id?} " ) ; routes.MapRoute( name: "out", template: "outbound/{controller=Home}/{action=Index}"); ));
Глава 16 . Дополнительные возможности маршрутизации 477 Представление , приведенное в листинге 16.10, содержит два якорных элемента, каждый из которых указывает те же самые контроллер и действие . Разница в том, что второй элемент применяет атрибут дескрипторного вспомогательного класса asp - rou t e для указания на необходимость использования маршрута ou t при гене­ рации URL, относящегося к атрибуту h ref. Листинг 16.10. Генерация URL в файле Result.cshtml @model Resul t @( Layout = null; < ! DOCTYPE html> <html> <head> <meta name="vi ewport" content="widt h=device- wi dth" /> <t itle>Rou ting</title> <l i nk re l="stylesheet " asp-h r ef- incl ude="liЬ/b o otstra p /dist/cs s / * .m i n.css" / > </head> <body cla ss=" pane l-body "> <tаЫе class="taЫe taЬle-bordered taЫe-striped taЫe-condensed"> <tr><th>Controller : </th>< t d>@Mod e l . Co nt roll e r</ t d></tr> <tr><th>Action : </th><td>@Model . Ac tion</td></ t r> @foreach (s tring key in Mode l. Data .Keys ) ( <tr> <th>@ ke y : < /th><t d >@Model.Data[key] </td>< / t r> </tаЫе> <а asp-controller="Home" asp-action="CustomVariaЬle"> This is an outgoing URL</a> <а asp-route="out">This is an outgoing URL</a> </body> </html> Атрибут a s p - rou te может применяться только в ситуации, когда отсутствуют ат­ рибуты asp - control l e r и as p - act i o n, т.е. выбирать можно лишь специфический маршрут для контроллера и действия , которые вызывали визуализацию представле­ ния. Запустив приложение и запросив URL вида / H ome / CustomVa r iaЬle , вы полу­ чит е дв а разных URL, которые сгенерировали маршруты: <а href=" /Home/Cus tomVariaЬle" >This is an outgoi ng URL</a> <а href="/outbound">This is an outgoi ng URL</a> Аргумент против именованных маршрутов Проблема с зависимостью от имен маршрутов при генерации исходящих URL связана с тем, что это нарушает принцип разделения обязанностей , который занимает центральное место в паттерне проектирования MVC. При генерации ссылки либо URL в представлении или ме­ тоде действия необходимо сосредоточиться на действии и контроллере , куда пользователь будет направлен, а не на формате URL, который будет использоваться. Привнося знание о различных маршрутах в представления или контроллеры, мы создаем зависимости, которых можно было бы избежать. В своих проектах я стараюсь не именовать маршруты (указывая n u ll для аргумента name ) и предпочитаю применять комментарии в коде, чтобы не забыть о том, для чего предназначен каждый маршрут.
478 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Генерация URL (без ссылок) Ограничение дескрипторных вспомогательных классов заключается в том, что они трансформируют НТМL-элементы и не могут быть легко переориентированы, если нужно генерировать URL для приложения без окружающей НТМL-разметки. Инфраструктура MVC предлагает вспомогательный класс, который можно исполь­ зовать для создания URL напрямую, доступный через метод Url . Action (),как де­ монстрируется в листинге 16 .11 . Листинг 16. 11 . Генерация URL в файле Resul t. cshtml @model Result @{ Layout = null ; < ' DOCTYPE html> <html> <head> <meta name= " viewpo rt" content= " width=device-width " /> <title>Routing</title> <link rel= "stylesheet" asp-href-include= "l iЬ/bootstrap/dist/css/* . min.css" /> </head> <body class= " panel-body"> <tаЫе class= " taЫe taЬle-bordered taЫe-striped taЬle-condensed " > <tr><th>Controller : </th><td>@Model.Controller</td></tr> <tr><th>Action:</th><td>@Model.Action</td></tr> @foreach (string key in Model . Data . Keys) { <tr><th>@key :< /th><td>@Model.Data[key]< /td></t r> </tаЫе> <p>URL: @Url.Action("CustomVariaЬle", "Home", new { id = 100 })</р> </body> </htrnl> В аргументах метода Url .Action () указываются метод действия, контроллер и значения для любых переменных сегментов. В результате добавления разметки, вы­ деленной полужирным в листинге 16.11, генерируется следующий вывод: <p>URL: /Home/CustomVariaЬle/100</p> Генерирование URL в методах действий Метод Url .Act ion () может применяться также и в методах действий для созда­ ния URL внутри кода С#. В листинге 16.12 модифицирован один из методов действий контроллера Horne, чтобы генерировать URL с использованием Url. Act ion (). Листинг 16.12. Генерация URL внутри метода действия в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class HomeCo ntroller : Controller
Глава 16. Дополнительные возможности маршрутизации 479 puЫic ViewResul t Index () => View ( "Resu l t ", new Result { Controller = nameof(HomeController) , Action = nameof(Index) }); puЫic ViewRes ult CustomV ariaЬle ( str ing i d) { Result r = new Result { }; Controller = nameof(HomeController) , Action = nameof(CustomVariaЬle) , r.Data["id"J = id ?? "<no value>"; r.Data["url"] = Url.Action("CustomVariaЬle", "Ноте", new { id = 100 }) ; return View ( "Resul t", r) ; Запустив пр иложение и запросив URL вида /Home/CustomVariaЬle, вы заметите, что в таблице появилась строка, которая отображает этот URL (рис . 16.4). [j Routil\g х ~ ' С ~a lhost:60588/Horne/CustornVarraЫe Controller: HomeController Action: CustomVariaЫe 1 id: <ПО value> 1 ~~l• /Hom&C"'tom,..V-/:-ro-i~-be/-le-~-~-:-o_m_V_a-ri_a_Ы_e_/1_0_0-. L ___ Рис. 16.4 . Генерация URL в методе действия Настройка системы маршрутизации Вы уже видели, насколько гибкой и конфигурируемой является система маршру­ тизации, но если она не удовлетворяет существующим требованиям, тогда ее поведе­ ние можно настроить. В этом разделе будут показаны два способа такой настройки. Изменение конфигурации системы маршрутизации В главе 15 вы узнали, как конфигурировать объект RouteOptions в файле Startup . cs для настройки специального ограничения маршрута. Объект RouteOpt i ons также применяется для конфигурирования ряда средств маршрутизации с помощью свойств, описанных в табл. 16.3.
480 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 16.З. Конфигурационные свойства RouteOptions Имя Описание AppendTrailingSlash Когда это свойство типа bool равно true, к генерируемым системой маршрутизации URL добавляется завершающий символ косой черты. Стандартным значением является false LowercaseUrls Когда это свойство типа bool равно true , результирующие URL преобразуются в нижний регистр, если контроллер, дейс­ твие или значения сегментов содержат символы нижнего ре­ гистра . Стандартным значением является false В листинге 16.13 приведен файл Startup . cs с добавленными операторами для настройки обоих конфигурационных свойств из табл. 16.3 . Листинг 16.1 З . Конфигурирование системы маршрутизации в файле Startup. cs using Microsoft . AspNetCore . Builder; using Microsoft.Extensions.Dependencyinjection; using Microsoft . AspNetCore . Routing . Constraints ; using Microsoft.AspNetCore . Routing; using UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.Configure<RouteOptions>(options => { options.ConstraintMap . Add("weekday ", typeof(WeekDayConstraint)) ; options.LowercaseUrls = true; options.AppendTrailingSlash = true; }); services.AddMvc(); puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages() ; app . UseDeveloperExceptionPage(); app .U seStaticfiles() ; app . UseMvc(routes => { routes.MapRoute( name: " default ", template : " {control ler=Home}/{action=Index)/{id?} " ) ; routes . MapRoute( name: "out" , ternplate: " outbound/{contr o ller=Horn e}/{action=Index)"); }); Запустив приложение и просмотрев URL, которые сгенерировала систе ма марш­ рутизации, вы увидите , что изменение конфигурационных свойств обес печило пре­ образование URL в нижний регистр и добавление к ним завершающей косой черты (рис . 16.5).
Глава 16 . Дополнительные возмо жн ости маршрутизации 481 D Rout ing х [--··--·-·--··· ---- ----·-·- ----····"------·· ----·-·---··l С . Ф locall1ost :б0588/Home/CunomVaпaЬle -Q- '---------------· -·---' Controller: Action: id: url: ~- -·~ ~-.... -' . -·-~· ~- --- - ....... - - ...... -- HomeController CustomVariaЫe <По value> /home/customvariaЫe/1001 1 URL: /home/cus1omvaпaЫe/1001 J !_________________ ____. - -- -·--- ---·-·----- ------·-- - --"-·-- Рис . 16.5 . Конфигурирование системы маршрутизации Создание специального класса маршрута Если вам не нравится способ. которым система маршрутизации производит сопос­ тавление с URL , или для вашего приложения необходимо реализовать что-то специ­ фическое, то можете создать собственные шшссы маршрутизации и использовать их для обработки URL. Инфраструктура ASP.NET предоставляет интерфейс Microsoft . AspNetCore . Routing . IRouter, который можно реализовать для создания специаль­ ного маршрута. Вот как выглядит определение интерфейса IRouter: using System . Threading . Tasks ; namespace Microsoft.AspNetCore.Routing puЬlic interface IRouter { Task RouteAsync(RouteContext context) ; VirtualPathData GetVirtualPath(VirtualPathContext context); Чтобы создать специальный маршрут, понадобится реализовать метод RouteAsync () для обработки входящих запросов и метод GetVirtualPath ().если нужно генерировать исходящие URL. В целях демонстрации мы создадим специальный класс маршрутизации, который будет обрабатывать запросы к унаследованным URL. Предположим, что мы перенесли КЮ<ое-то существующее приложение в MVC, но есть пользователи, которые поместили старые URL в закладки или жестко закодировали их в сценариях. Мы хотим по-пре­ жнему поддерживать такие старые URL. Мы могли бы обработ ать это с применением обычной системы маршрутизации, но решение данной задачи будет хорошим приме­ ров для целей настоящего раздела. Маршрутизация входящих URL Чтобы понять, Iiaк работают специальные маршруты, мы начнем с создания одного такого маршрута, который будет обрабатывать каждый аспект запроса самостоятель­ но, не используя контроллер и представление. Для этого в папке Infrastructure со­ здается файл класса по имени LegacyRoute . cs с реализацией интерфейса IRouter, приведенной в листинге 16 . 14 .
482 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 16.14. Содержимое файла LegacyRoute. cs из папки Infras tructure us ing Mic r osoft. AspNe tCore.Http; usi ng Microsoft.AspNetCore .Routing; using System ; using System . Linq ; u sing System.Text ; u sing System. Threadi n g . Tasks ; n amespace Ur l sAndRoutes .Inf r a s truct u re puЫic class LegacyRout e : IRouter { private s tring[ ] urls ; puЬlic LegacyRo ute(pa rams s tri ng [] targetUrls ) { this.urls = targetUrls; puЫic Ta s k Ro u teAs ync (RouteCo ntext con text) { string requested Url = conte xt .H ttpConte xt . Request . Path . Value.TrimEnd('/ '); if (urls . Contains( r equestedUrl , StringComparer . OrdinalignoreCase) ) context .Handler = async ctx => HttpResponse response = ctx . Respon s e ; ); byte [] bytes = Encoding .ASCII . GetByt e s ($ "URL : {reque stedUrl }") ; awa i t r espons e.Body .WriteAsync(bytes , О, bytes . Length) ; r eturn Task.Compl etedTask; puЫic Vi rtua lPathData GetVirt ua lPat h (Virtua lP athContext contex t) ( return n ull; Класс LegacyRoute реализует интерфейс IRouter, но определяет код только для метода Rout e Async () , который применяется для обработки входящих запросов; вскоре мы добавим поддержку и для исходящих URL . В методе RouteAsync () находится совсем немного операторов, но в своей р аботе они полагаются на несколько важных типов ASP.NET . Лучше всего начать исследова­ ние с сигнатуры метод а : pu Ы ic as ync Tas k RouteAsyn c( RouteContext context ) { Метод Rou t eAsync ( ) отвеча ет за оценку, может ли запрос быть обработан, и если может, то за управление процессом, посредством которого генерируется отв ет, отправ­ ля емый обратно клиенту. Такой проц е сс выполняется асинхронно , по этому ме тод RouteAsync () возвращает объе1п Ta s k . Метод RouteAsync () вызывается с аргументом Ro u teContext , который предо­ ставляет доступ ко всему, что известно о запросе, и предлагает средства , требу емые для отправки ответа клиенту . Класс Rou teConte xt находится в пространстве имен Mi crosoft . AspNetCore . Routing и определяет три свойства, описанные в табл. 16.4 .
Глава 16 . Дополнительные возмож ности маршрутизации 483 Таблица 16.4 . Свойства, определенные в классе RouteContext Имя RouteData HttpContext Handler Описание Это свойство возвращает объект Microsoft . AspNetCore . Routing . RouteData. При написании специального маршрута, который полагается на средства MVC ( как объясняется в следующем разделе) , данный объект используется для определения контроллера, метода действия и аргументов, применяемых для обработки запроса Это свойство возвращает объект Microsoft . AspNetCore . Http . HttpContext, который предоставляет доступ к деталям НТТР ­ запроса и предназначен для выпуска НТТР-ответа Это свойство используется для предоставления системе маршру­ тизаци и объекта RequestDelegate , который будет обрабатывать запрос. Если метод RouteAsync не устанавливает данное свойство, тогда система маршрутизации продолжит работу своим способом посредством набора маршрутов в конфигурации прило жения Система маршрутизации вызывает метод RouteAsync () каждог о маршрута в приложении и после каждого вызова проверяет значение свойства Handler. Если это свойство установлено в RequestDelegate, тогда маршрут пр едоставл я ет систе­ ме маршрутизации делегат, который может об работать запрос, и такой деле гат вызы­ вается для генерации ответа. Ниже показана сигнатур а делегата RequestDelegate, определенного в пространстве имен Microsoft . AspNetCore. Http : using System . Threading .Tasks ; namespace Microsoft .AspNetCore . Http puЫ ic delegate Task RequestDelegate(HttpContext context) ; Делегат RequestDelegate прини мает объект HttpContext и возвращает объект Task, который будет генерировать ответ. Если свойство Handler н е установлено ни в одном из мар шрутов, то системе маршрутизации известно, что пр иложение не может обр аботать запрос, и он а сгенерирует ответ 404 - Not Found (404 - не найдено). С этой целью реализация метода RouteAsync () должна установить, может ли она обработать запрос, для чего обычно требуется объект HttpContext. В рассматрива­ емом примере применяется свойство HttpContext . Request, котор ое воз враща ет объект Microsoft . AspNetCore . Http . HttpRequest, описывающий запрос. Объ ект HttpRequest предоставляет доступ ко всей информации о запрос е, включая заго ло в­ ки , тело и детали того, откуда запрос произошел , но мы заинтересованы в свойстве Path, пос кольку оно дает подробны е сведения относительно URL , зап рошенного кли­ ентом. Свойство Path возвращает объект PathString, предлагающий удобные ме­ тоды для объединения и сравнения путей URL . Мы ис пол ьзуем свойство Value , т.к. оно дает полный раздел пути URL в виде строки, которую можно сравнить с набором поддерживаемых URL, полученным конструктором LegacyRoute. string requeste.dUrl = context.HttpContext.Request. Path.Value . TrimEnd ( ' / ' ) ; if (urls . Contains( requestedUrl, StringComparer . Ordina li gnoreCase)) { Здесь с помощью метода TrimEnd () из URL удаляется завершающая косая черта (если она есть) . которая могла быть добавлена либо пользователем, либо в результате
484 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC установки конфигурационного свойства AppendTrailingSlash, описанного в разде­ ле "Изменение конфигурации системы маршрутизации" ранее в главе. Если запрошенный путь входит в число сконфигурированных для поддержки клас­ сом LegacyRoute , тогда мы устанавливаем свойство Handler с применением лямбда ­ функции, которая будет генерировать ответ: context.H andler = async ctx => { HttpResponse response = ctx . Response; }; byte(] bytes = Encoding.ASCII.GetBytes($"URL: {requestedUrl}"); await response.Body.WriteAsync(bytes , О , bytes.Length); Свойство HttpContext. Response возвращает объект HttpResponse , который может использоваться для создания ответа клиенту и предоставляет доступ к заголов­ кам и содержимому, подлежащему отправке клиенту. Метод HttpResponse . Body . Wr i teAsync () применяется для асинхронной записи в качестве ответа простой стро­ ки ASCII. Такой прием не будет предприниматься в реальном проекте. но он позволяет выпустить ответ, не выбирая и не визуализируя представления (хотя в следующем разделе будет ПОI{азано, как это делать). Когда свойство Handler установлено, системе маршрутизации известно, что ее поиск маршрута закончен и можно вызывать делегат для генерации ответа клиенту. Применение специального класса маршрута Расширяющий метод MapRoute (), который до сих пор использовался для со­ здания маршрутов, не поддерживает применение специальных классов маршрутов. Чтобы воспользоваться классом LegacyRoute , придется задействовать другой подход (листинг 16.15). Листинг 16.15. Применение специального класса маршрутизации в файле startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft.Extensions.Dependencyinjection ; using Microsoft . AspNetCore.Routing . Constraints ; using Microsoft . AspNetCore . Routing ; using UrlsAndRoutes . Infrastructure ; namespace UrlsAndRoutes puЫic class Start up { puЫic v oid Config u reServices(I ServiceCollection services) { services . Configure<RouteOptions>(options => { options . ConstraintMap . Ad d( " wee kday ", typeof(WeekDayConst r aint)) ; opt i ons .Lowe rcaseUrls = true; options . AppendT r ail i ngSlash = true; }); services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app .U seStatusCodePages() ; app.UseDeveloperExceptionPage(); app.UseStaticFi l es() ;
Глава 16. Дополнительные возмо ж ности маршрутизации 485 app . UseMvc(routes => routes.Routes.Add(new LegacyRoute( 11 /articles/Windows 3.1 Overview.html", 11 /otd/.NET_l .O_Cla;s_Library")); routes . MapRo ute( name : "default", template : " {controller=Home }/{action= Index }/{ i d?}" ) ; routes.MapRoute( name : "out", template : " outbo und/{con t r o lle r=Home)/{act i on=I ndex) "); }); При использовании специальных классов потребу ется вызвать метод Ad d () на коллекции маршрутов, чтобы зар егистрировать класс реализации IRou te r. В этом пример е аргументами конструктора LegacyRo u te являются унаследованные URL, ко­ торые должен поддерживать специальный маршрут. Запустив приложение и запросив /articles/W i ndow s_ З . l _ Ove r view . h tml, можно увидеть результат. Специальный м а ршрут отобра з ит запрошенный URL (рис. 16.6). [J l0<alh ost:60S88/ article s: Х l~f- ·. С Lso_~~c~~~t:бOSB~:r~~cles.No~doщs;~.1_0verv1e111.html -~ : --·-·--------~---··--· ~---- -----~~---- · - · ... •· - -- -- UR L: /articles/Windows_З . l_Overview .h tml J ---- --" -- ----- ---- ---- ----- ---- · Рис. 16.6. Использование специального маршрута Маршрутизаци я на контроллеры MVC Существует крупная брешь между сопоставлением с простыми строками URL и применени ем системы I{QНТроллеров , действий и представлений Razor инфраструк­ туры MVC. К счастью, при создании специальных маршрутов реализовывать такую функциональность самостоятельно не понадобится , потому что для выполнения всей тяжелой работы может быть задействован класс, который MVC использует " за кули­ сами". Чтобы подготовиться к применению инфраструктуры МVС, добавьте в папку Control l ers файл класса по имени Le gacyCon t r ol l er . cs с содержимым, приве­ де нным в листинге 16.16. Листинг 16.16 . Содержимое файла LegacyController.cs из папки Controllers using Mic r osoft . AspNetCore . Mvc ; namespace UrlsAndRo utes . Contro lle r s puЬlic class LegacyControl ler : Control l e r puЫic ViewRes u lt GetLegacyUrl(s t ri ng legacyUrl) => Vi ew((object)legacyUrl) ;
486 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Метод действия GetLegacyURL () в этом простом контроллере принимает п ара ­ метр, который содержит унаследованный URL, запрошенный клиентом. Если бы да н­ ный контроллер был реализован в реальном про екте, то упомянутый метод использо­ вался бы для извлечения запрошенных файлов, но здесь просто отображается URL в представлении. Совет. Обрат и те внимание , что в листинге 16 .16 аргумент метода View() приводится к типу obj ect. Одна из перегру женных версий метода View () прини мает параметр типа s tr ing, указывающий имя представления для визуализации , и без приведения компи­ лятор С# выберет именно ее. Во избежание этого мы выполняем приведение к obj ect, чтобы однозначно вызывалась перегруж енная версия, которая принимает модель пред­ ставления и применяет стандартное представление . Проблему можно было бы также решить за счет использования перегруже нной версии, принимающей как и м я представ­ ления, так и модель представления, но предпочтительнее не делать явны х ассоциаций между методами действий и представлениями , если это возможно. За дополнительными сведениями обращайтесь в главу 17. Создайте папку Views/Legacy и поме стит е в не е файл пр едс тавл ения по имени GetLegacyUrl. cshtml с содержимым из листинга 16. 17. Новое представление отоб­ ражает значение модели, которое покажет запрошенный клиентом URL . Листинг 16.17. Содержимое файла GetLegacyUrl.cshtml из папки Views/Legacy @model string @{ Layout = null; < !DOCTYPE html> <html > <head> <meta name="vi ewport " content= " width=device-width " /> <title>Routing</titl e> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/* . min . css " /> </head> <body class= "panel - body " > <h2>GetLegacyURL</h2> Th e URL requested was : @Model </body> </html> В листинге 16.18 класс LegacyRoute модифицирован тю<, чтобы обрабатываемые им URL маршрутизировались на действие GetLegacyUr 1 конт ролл ера Legacy. Листинг 16.18. Маршрутизация на контроллер в файле LegacyRou te. cs using Microsoft . AspNetCore . Http ; using Microsoft . AspNetCore.Routing ; using System; using System .Li nq ; using System.Text ; using System.Threading.Tasks ; using Microsoft . AspNetCore.Mvc . Internal; using Microsoft.Extensions.Dependencyinjection ; namespace UrlsAndRo utes.Infrast ru cture {
Глава 16. Дополнительные возможности маршрутизации 487 puЫic class LegacyRoute : IRouter private string[] urls; private IRouter mvcRoute; puЫic LegacyRoute(IServiceProvider services, params string[] targetUrls) this.urls = targetUrls ; mvcRoute = services.GetRequiredService<MvcRouteHandler>(); puЫic async Task RouteAsync(RouteContext context) { string requestedUrl = context.HttpContext . Request . Path . Value.TrimEnd('/'); if (urls.Contains(requestedUrl, StringComparer . OrdinalignoreCase)) context.RouteData.Values["controller"] = "Legacy"; context.RouteData.Values["action"] = "GetLegacyUrl"; context.RouteData.Val.ues["l.egacyUrl"] = requestedUrl; await mvcRoute.RouteAsync(context); puЫic VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; Класс Microsoft . AspNetCore. Mvc. In ternal. MvcRouteHandler предоставляет механизм, посредством которого переменные сегментов controller и action при­ меняются для нахождения класса контроллера, выполнения метода действия и воз­ вращения результата клиенту. Этот класс был написан таким образом. что к нему можно обращаться из специальной реализации IRouter, которая предоставляет зна­ чения cont roller и action, а также любые другие требующиеся значения, подоб­ ные аргументам метода действия. В листинге 16.18 создается новый экземпляр класса MvcRouteHandler, которому делегируется задача нахождения :класса контроллера. Для этого необходимо предо­ ставить данные маршрутизации: context.RouteData . Values["controller" ] = " Legacy"; context . RouteData.Values[ "action"] = "GetLegacyUrl"; context.RouteData.Values["legacyUrl"] = requestedUrl; Свойство RouteContext. RouteData. Vales возвращает словарь. который исполь­ зуется для предоставления значений данных классу MvcRouteHandler. В стандарт­ ной системе маршрутизации значения данных создаются путем применения шаблона URL к запросу, но в специальном :классе маршрута значения жестко закодированы, таR что всегда производится направление на действие GetLegacyUrl контроллера Legacy. Между запросами изменяется только значение данных legacyUrl, которое устанавливается в URL запроса; оно будет применяться в качестве аргумента с таким же именем, получаемого методом действия.
488 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Последнее изменение в листинге 16.18 делегирует ответственность за поиск и ис­ пользование класс а контроллера для обработки запроса. await mvcRoute.RouteAsync( context) ; Объект RouteContext , теперь содержащий значения control l er , action и legacyUrl, передается методу RouteAsync () объекта MvcRouteHandler, который берет на себя обязанность за любую дальнейшую обработку запроса, включая уста­ новку свойства Handler. В результате класс LegacyRoute может сосредоточиться на решении, какие URL он будет обрабатьmать, не имея дела с деталями работы с конт­ роллерами напрямую. Объект MvcRou teHandler, выполняющий работу в расс мат р иваем ом пример е , должен запрашиваться как служба, что объясняется в главе 18. Чтобы предоставить конструктор LegacyRoute с объектом реализации IServiceProvider , который не­ обходим ему для создания объекта MvcRouteHandler, оператор, определяющий мар­ шрут в классе Startup, обновлен с целью снабжения его возможностью доступа к службам приложения (листинг 16.19). Листинг 16.19. Обеспечение доступа к службам приложения в файле Startup. cs puЫic void Configure(IApplicat ionBuilde r арр) { app .U seStatusCodePages(); app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvc(routes => { routes.Routes . Add(new LegacyRou te ( app.ApplicationServices, " /articles/Wi n dows_З . l _Ove rview .html", "/old/.NET 1.0 Class Library")); routes . MapRoute( name: " default ", template : " {control l er=Home}/{action=Index }/{id?} " ) ; routes . MapRoute( name : "out", template : " outbound/{controller=Home}/{act i on=Index} " ) ; }); Запустив приложение и снова запросив / articl es/Windows_ 3 .1 _0verview. htrnl, вы увидите, что простой текстовый ответ был заменен выводом из представления (рис. 16.7). GetlegacyURL Th e URI _ requested was: raгtic les/Windows_з. 1 _overview.html 1 ---~----------·--- --~----... · -· ---- --· ---- --· ---- ------ ------ --. . . . . 1" Рис. 16.7 . Делегирование работы с контроллерами и представлениями
Глава 16 . Доnолнительные возможности маршрутизации 489 Генерация ИСХОДЯЩИХ URL Для поддержки генерации исходящих URL мы должны реализовать в классе LegacyRoute метод GetVirt ualPath () ,как показано в листинге 16.20. Листинг 16.20. Генерация исходящих URL в файле LegacyRoute. cs using Microsoft . AspNetCore . Http ; using Microsoft .As pNetCore .Rout ing ; using System ; using System .Lin q ; using System .Text ; using System .T hreading.Tasks ; using Microsoft.AspNetCore . Mvc .Inte rn a l; using Microsoft . Extensions .Dependencyinjection ; namespace UrlsAndRoutes.Infrastructure puЬlic class LegacyRoute : I Router { privat e s tr ing [] urls; private IRou t er mvcRoute ; puЫic LegacyRoute(IServiceProvider servi ces , params string [ ] targetUrls ) this . urls = targetUrls ; mvcRoute = services . GetRequiredService<MvcRouteHandler>(); puЫic async Task Rou teAsync(RouteCo ntext context) { string requestedUrl = context.HttpContext.Request . Path .Value.TrimEnd('/'); if (urls . Contains(requestedUrl, Str i ngComparer . Or di n a lignoreCase)) context . RouteData . Values (" controller"J = "L egacy "; context . RouteData .V a l ues[ " action " J = "G etLegacyUrl "; context . RouteData . Va lues ["legacyUrl "J = requestedUrl ; await mvcRoute.RouteAsync(context); puЬlic VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context.Values.ContainsKey("legacyUrl")) { string url = context.Values["legacyUrl"] as string; if (urls.Contains(url)) { return new VirtualPathData(this, url); return null; Система маршрутизации вызывает метод GetVirtualPath () каждого маршрута , который был определен в классе Startup , давая каждому шанс сгенерировать исхо­ дящий URL, требующийся приложению. Аргумент метода GetVirtualPath () - это объект Virtua l PathContext , который предоставляет необходимую информацию от­ носительно URL. В табл . 16.5 описаны свойства класса VirtualPathContext.
490 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 16.5 . Свойства, определенные в классе VirtualPathContext Имя RouteName Val ues AmЬientValues HttpContext Описание Это свойство возвращает имя маршрута Это свойство возвращает индексированный по имени словарь значений , которые могут применяться для переменных сегментов Это свойство возвращает словарь значений, которые полезны для гене­ рации URL, но не будут встраиваться в результат. При реализации собс­ твенного класса маршрутизации такой словарь обычно пуст Это свойство возвращает объект HttpContext, который предоставляет информацию о запросе и ответе, находящемся в процессе подготовки В рассматриваемом примере свойство Vа 1 u е s используется для получения значения по имени legacyUrl, и если оно соответствует одному из URL, кото­ рые были сконфигурированы для поддержки маршрутом, тогда возвращается объ­ ект VirtualPathData, снабжающий систему маршрутизации деталями URL . Аргументами конструктора класса VirtualPathData являются объект реализации IRouter, который генерирует URL , и сам URL. return new VirtualPathData( this, url ) ; В листинге 16.21 показано измененное представление Resul t . cshtml для запра­ шивания исходящих URL, которые нацелены на специальное представление. Листинг 16.21. Генерация исходящих URL из специального класса маршрута в файле Resul t. cshtml @model Resu l t @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>Routing</title> <link rel= " stylesheet " asp - href - include= " liЬ/bootstrap/dist/css/* . min . css " /> </head> <body class= " panel - body " > <tаЫе class= " taЫe taЫe - bordered ta Ыe -strip ed taЬle - condensed " > <tr><th>Contro l ler : </th><td>@Model . Cont roll er</td></tr> <tr><th>Action:</th><td>@Model . Action</td></tr> @foreach (string key in Model . Data . Keys) { <tr><th>@key : </th><td>@Model . Data[key]</td></tr> </tаЫе> <а asp-route-legacyurl="/articles/Windows_З.l_Overview.html" class="Ьtn Ьtn-primary">This is an outgoing URL </а> <p>URL: @Url.Action(null, null, </body> </html> new { legacyurl = 11 /articles/Windows_З.l Overview.html"})</p>
Глава 16. Дополнительные воз м ожности маршрутизации 491 В этом пример е нет необходимости указывать дескрипторному вспомогательному классу контроллер и действие для исходящего маршрута, потому что при генерации URL они не применяются. Учитьmая это, атрибуты дескрипторного вспомогател ьного класса asp - control ler и asp - action в элементе а опушены. Когда генерируется только URL, первые два аргумента для вспомогательного метод а Url . Action () уста­ навливаются в null по той же причине. Если вы запустите приложение и просмотрите НТМL-размет1{у, полученную в от­ вет на запрос стандартного URL, то увидите, что для создания URL использовался специальный класс маршрута: <а class= " Ьtn Ьtn - primary " hr ef= "/articles/windows_З .l_overview.html/" > This is an outgoing URL </а> <p>URL : /articles/windows_З.l_overview.html/ </p> Завершающие косые черты, добавленные к URL, являются результатом уста­ новки в true конфигурационного свойства AppendTrailingSlash внутри файла Startup . cs, и важно, чтобы с опоставление входящего ма ршрут а было спо с обно со­ ответствовать URL с добавленным символо м косой черты. Совет. Если URL, который вы видите в НТМL-ответе, имеет отличающийся формат, такой как /?legacyurl = %2Farticles %2FW indows 3 . l _Overview .html , тогда для ге­ нерации URL специальный маршрут не применялся , а вместо него был вызван какой-то другой маршрут в прило жении. Поскольку никакого контроллера или действия не было у казано, произошло нацеливание на действие Index контроллера Home, и к строке за­ проса URL добавилось значение legacyUrl. В таком случае удостоверьтесь в установ­ ке свойства IsBound в true внутри метода GetVirtualPath () , и проверьте , что в конфигурации внутри файла Startup. cs указаны корректные URL для конструктора класса LegacyRoute, а специальный маршрут определен перед всеми остальными маршрутами . Работа с областями Инфраструктура ASP.NET Core MVC поддержив ает организацию веб-прилож ения в виде областей, где каждая область представляет функциональный сегмент приложе­ ния, такой как администри рование , выписка счетов- фактур. поддержка пользовате­ лей и т.д. Это удобно в крупных проектах, в которых наличие единственного набора папок для всех контроллеров, представлений и моделей может привести к сложнос­ тям в управлении. Каждая область МVС имеет собственную структуру папок, позволяя все хранить отдельно. Такой подход делает более очевидным то, 1<акие элементы проекта относят­ ся к каждой функциональной области приложения, и помогает множеству ра зраб от­ чиков трудиться над проектом, не конфликтуя друг с другом. Области в значительной степени поддерживаются системой маршрутизации и потому они рассматриваются вместе с URL и маршрутами. В настоящем разделе будет показано, как наст раивать и пользоваться областями в проектах MVC.
492 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Создание области Создание области требует добавления папок в проект. Папка верхнего уровня на­ зывается Areas . Внутри н ее для каждой необходимой области пр едусмотр ен а своя пап ка, содержащая собственные подпапки Con t rollers , Vi ews и Models. Мы пла ­ нируем создать обл асть по имени Adrnin , что означает создание набора папок, опи­ санных в табл. 16 .6 . Для подготовки примера проекта со здайте все папки, перечис­ л енные в табл. 16.6 . Таблица 16.6 . Папки, требуемые для подготовки областей Имя Areas Ar eas/Admin Areas/Adrnin/Controllers Ar eas/Admin / Views Ar e as/Adrnin/Views/ Horne Ar e as/Admi n/Model s Описание Эта папка будет содержать все области в приложении MVC Эта папка будет содержать классы и представления для области Admin Эта папка будет содержать контроллеры для области Admin Эта папка будет содержать представления для области Admin Эта папка будет содержать представления для контролле­ ра Horne в области Admin Эта папка будет содержать модели для области Admin Хотя каждая область применяется по отдельности, многие средства МVС полагают­ ся на стандартны е функциональные возможности С# или .NET . таки е как пространс­ тва им ен . Чтобы обл егчить использование области. первым дело м нужно добавить файл импортирования представлений, который позволит работать с моделями из об ­ ласти внутри пр едставлений, не включая пространства имен и получая преимущест­ во от применения дескрипторных вспомогательных классов . Создайте в папке Areas/ Adrnin/Views файл импортирования представлений по имени _Viewirnports . cshtml и поместите в него операторы, приведенные в листинге 16.22 . Листинг 16.22. Содержимое файла_Viewimports. cshtrnl из папки Areas/Admin/Views @us i ng Url s AndRoutes . Areas.Admi n. Models @addTagHelper * , Mi crosoft . As pNetCore. Mvc . Ta gH elper s Создание маршрута для области Чтобы задействовать области , потребуется добавить в файл Startup . cs маршрут, который содержит пер еменную сегм ента a r ea (листинг 16.23). Листинг 16.23. Добавление маршрута для областей в файле Startup. cs us ing Microsof t. AspN e tCore . Bu ilder ; u sing Microsoft . Exten sions . Dependencyi njection ; u sing Microsoft.AspNetCore . Routing . Constra i nts ; u sing Microsoft.AspNetCore . Routing ; u sing UrlsAndRoutes . Infras tru c t ure ;
namespace UrlsAndRoutes puЫic class Startup { Глава 16 . Дополнительные возможности маршрутизации 493 puЫic void ConfigureServices(IServiceCollection services) { services . Configure<RouteOptions>(options => { optio11s . ConstraintMap.Add( 11 weekday 11 , typeof(Wee kDayConstra i nt)) ; options . LowercaseUrls = true ; options . AppendTrail i ngSlash = true ; }); services .AddMvc() ; puЫic void Configure(IAppl i cationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStat i cFiles() ; app . UseMvc(routes => { routes.MapRoute( name: " areas 11 , template: 11 {area:exists}/{controller=Home}/{action=Index} 11 ); routes.Rou t es . Add(new LegacyRoute( app . ApplicationServices , 11 /articles/Window s_ З . l _Overview .h tm l 11 , 11 /old/. NET 1. O _Class Library 11 )); routes . MapRoute( name : " default 11 , temp l ate : " {contro ller=Home}/{act i on=Index}/{ i d?} " ) ; routes . MapRoute( name: " out ", template : " outbound/ {controlle r=H ome}/{act ion=Index} 11 ); }); Переменная сегмента area используется для сопоставления с URL, которые наце­ лены на контроллеры в специфических областях. В листинге задействован стандар­ тный шаблон URL, но сегмент area можно добавлять в любой требующийся шаблон. Маршрут, который добавляет поддерж1tу областей, должен находиться перед менее специфическими маршрутами для обеспечения корректного сопоставления с URL. Ограничение exists применяется для гарантии того , что запросы соответствуют только областям, которые были определены в приложении. Заполнение области Контроллеры, представления и модели в области можно создавать точно так же, как в главной части приложения MVC . Чтобы создать модель, щелкните правой кноп­ кой мыши на папке Areas/Admin/Models, выберите в контекстном меню пункт Add <=:>Class (Добавить <=:> Класс) и создайте файл класса по имени Person . cs, содержи­ мое которого показано в листинге 16 .24 .
494 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 16.24. Содержимое файла Person. cs из папки Areas/Admin/Models namespace UrlsAndRoutes . Areas .Admin . Models puЫic c l ass Person { puЬlic string Name { get ; set ; puЫic string City { get ; set ; Для создания контроллера щелкните правой кнопкой мыши на папк е Areas/ Admin/Controllers, выберите в контекстном меню пункт AddqClass и создай­ те файл класса по имени HomeController. cs с определением контроллера из листинга 16.25. Листинг 16.25. Содержимое файла HomeController. cs из папки Areas/Admin/Controllers using Mic r osoft . AspNetCore . Mvc ; us ing UrlsAndRoutes . Areas . Admin . Models; namespace UrlsAndRoutes . Areas . Admin . Control l ers [Area ( "Admin 11 )] puЫic class HomeController : Contro ller { pr i vate Person [ ] data = new Person[] { new Person { Name 11 Alice ", City = "L ondon " }, new Person { Name "ВоЬ11, City 11Paris11 }, new Person { Name 11 Joe 11 , City = 11 New York11 }; puЫic ViewResult Index () => View (data) ; Новый контроллер в целом похож на стандартный кроме одного аспекта. Чтобы ас­ социировать контроллер с областью, к классу должен быть применен атрибут Area : [Area(11Admin11 ) ] puЫic class HomeController : Control l er { Без атрибута Area контроллеры не будут принадлежать обл асти, даже если они не определены в главной части приложения. Отсутствие атрибута Area может привести к странным результатам. Это первое, что следует проверять , если при работе с облас­ тями получаются не те результаты, которые ож идались. Совет. Если для настройки маршрутов используются атрибуты, как было описано в гла­ ве 15, тогда с применением маркера [ area J в аргументе атрибута Rou te можно ссы­ латься на область , указанную с помощью атрибута Area : [ Route ( 11 [ area] /арр/ [controller]/actions/[action] / {id : weekday?} 11 )]. Н аконе ц, добавьте в папку Areas/Admin/Views/Home файл представления Razor по имени Index. csht ml, содержимое которого показано в листинге 16 .26.
Глава 16 . Доnолнительные возмо ж ности маршрутизации 495 Листинг 16.26. Содержимое файла Index.cshtml из nanкиAreas/Admin/Views/Home @model Person [ ] @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name= " viewpor t " content=" wi dth=devi ce- wi dth" /> <ti tle>Areas</title> <link rel =" stylesheet" a s p- href- include=" liЬ/ boo ts trap/dist/ css/* .mi n. css " /> </head> <body class=" panel- body" > <tаЫе class="taЫe taЫe-bordered taЬle-striped taЫe-condensed"> <tr ><th>Name</th><th>City</ t h>< / t r> @foreach (Person р in Model) { <tr><td>@p . Name</td><td>@p . Ci ty</ td></t r > </tаЫе> </body> </html> Моделью для данного представления является массив объектов Person. Благодаря файлу импортирования представлений , содержимое которого бьmо приведено в листин­ ге 16.21, есть возможность ссылаться на тип Per son без указания пространства имен. Запуск приложения и запрос URL вида /Admin даст результат. показанный на рис . 16.8. Name Alice ВоЬ Joe City London Paris New York Рис. 16.8. Использование области Влияние области на приложение MVC Важно понимать влияние, которое области оказывают на остальные части приложения. Мы создали область по имени Admin , но в главной части приложения имеется также контрол­ лер Admin. До создания области запрос /Adm in направлялся бы действию Index конт­ роллера Adm i n в главной части приложения; теперь он будет нацелен на действие I ndex контроллера Ho me в области Adm i n (корень области предоставляет стандартные значения для переменных сегментов con troller и act i on ). Изменение такого вида может стать причиной неожиданного поведения, и наилучший способ применения областей предусмат­ ривает встраивание их использования в первоначальную схему именования контроллеров для проекта. Если возникла потребность добавить области в готовое приложение, то вы должны тщательно обдумать их влияние на имеющиеся маршруты.
496 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Генерирование ссылок на действия в областях Для создания ссылок, указывающих на действия в той же области, к которой от­ носится текущий запрос, никаких дополнительных шагов предпринимать не придет­ ся. Инфраструктура МVС обнаруживает, что текущий запрос относится к конкретной области, и гарантирует, что средство генерации исходящих URL будет искать соот­ ветствие только среди маршрутов, определенных для этой области . Например , в лис­ тинге 16.27 демонстрируется добавление элемента а в файл Index. cshtml из папки Areas/Admin/Views/Home. Листинг 16. 27. Добавление якоря в файле Index. cshtml из папки Areas/Admin/Views/Home @mode l Pe rson [] @{ Layout = null ; <!DOCTYPE html > <html > <head> <meta name="viewport" content= " width=devi ce-width " /> <title>Areas </t itle> <link rel=" styl esheet " asp -hr e f-i nc l ude= "li Ь/bootstrap/dist/css/* . min . css " /> </ he ad> <body class= "panel -body " > <tаЫе c l ass= " taЫe taЫe - bordered taЬle-striped taЫe-condensed"> <tr><th>Name</th ><th>C i ty</th></tr> @foreach (Person р i n Model) { <tr><td>@p .Name </ td><td>@p .City</td></tr> </tаЫе> <а asp-action="Index" asp-controller="Home">Link</a> </body> </html > Запустив приложение и запросив URL вида / a d.mi n, вы заметите, что ответ содер­ жит следующий элемент: <а href= " /admin/ " >Link</a> При генерации исходящей ссылки система маршрутизации выбрала маршрут для области и учла стандартные значения, доступные для переменных сегментов controller и action . Чтобы создать ссьmку на действие в другой области или в главной части прило­ жения, вы должны снабдить систему маршрутизации значением для сегмента area (листинг 16.28). Листинг 16.28. Нацеливание на другую область в файле Index. cshtml из папки Areas/Admin/Views/Home @model Person [] @{ Layo ut = null; <!DOCTYPE html > <html > <head>
Глава 16 . Дополнительные возможности маршрутизации 497 <meta name= " viewport " content= " width=device - width " /> <title>Areas</title> <link rel=" styl esheet " asp - href -i nc lude= " liЬ/bootstrap/dist/css/*.min . css " /> </head> <body class= " panel-body " > <tаЫе class= " taЫe taЫe-bordered taЫe -str ip ed taЫe-condensed " > <tr><th>Name</th ><th>C i ty</ th> </t r> @foreach (Person р in Mode l) { <t r ><td>@p.Name </ td><td>@ p.City</ td>< / tr> </tаЫе> <а asp - action="Index" asp - co ntroll e r="Home " > Link</a> <а asp-action="Index" asp-controller="Home" asp-route-area="">Link</a> </body> </html> Атрибут asp - route - a r ea устанавливает значение для переменной сегмента area. В данном случае эта переменная устанавливается в пустую строку, что указывает на главную часть приложения и дает следующий НТМL-элем ент: <а href=" / " >Link</a> Если вы располагаете в своих контроллерах множеством областей и хотите марш­ рутизировать на них, тогда вместо пустой строки используйте имя нужной области. Полезные советы относительно схемы URL После всего сказанного у вас может все-таки остаться вопрос: с чего начинать про­ ектирование собственной схемы URL? Вы можете просто принять стандартную схему, но организация собственной схемы обеспечивает ряд пр еимуществ. В последние годы проектное решение с URL приложения применяется все более широко, и появилось несколько важных принципов проектиров ания . Следуя этим принципам проектиро­ ваниЯ , вы улучшите удобство использования, совместимость и поисковый рейтинг своих прилож ений. Делайте URL чистыми и понятными человеку Пользователи обращают внимание на URL в приложениях. Просто вспомните, ког­ да вы в последний раз пытались отправить кому-то URL веб-сайта Amazon. Вот как выглядит URL для предыдущего англоязычного издания этой книги : http : //www.amazon . com/Pro - ASP - NET - Experts - Voice - ASP -N et/dp/1430265299 Отправить по электр онной почте такой URL - еще куда ни шло , но попробуйте продиктовать его по телефону. Когда этим приходится заниматься часто, в итоге про- ще сообщить номер ISBN книги и предложить самостоятельно найти ее страницу на веб-сайте Amazon. Было бы неплохо получать доступ к странице книги с помощью URL следующего вида: http : //www . amazon .com/books/pro - aspnet - mvcS - framework Такой URL вполне моJtено было бы диктовать по телефону, и он не выглядит так , будто при наборе почтового сообщения на клавиатуру что -то свалилось.
498 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC На заметку! Чтобы было предельно ясно: я глубоко уважаю компанию Amazoп, которая про ­ дает больше моих книг, чем все остальные вместе взятые. Мне известен факт, что все сотрудники Amazoп - замечательно умные и приятные люди . Никто из ни х не окажется до такой степени мелочным, что прекратит продажи моих книг из-за настолько несуществен­ ного, как критика применяемого в Amazon формата URL . Я люблю Amazon. Я преклоняюсь перед Amazoп . Я просто х очу , чтобы они привели в порядок свои URL . Ниже даются руководящие принципы построения дружественных к пользователям URL . • Проектируйте URL так, чтобы они описывали предоставляемое содержимое , а не детали реализации приложения. Используйте /Articles/AnnualReport , а не /Websi te_ v2/CachedContentServer/FromCache/AnnualReport . • Отдавайте предпочтение заголовкам содержимого п ер ед идентификационными номерами . Применяйте /Artic le s/AnnualReport , а не /Articles/2392. Если вы должны использовать идентификационные номера (для различения элемен­ тов с одинаковыми заголовками или во избежание дополнительных запросов к базе данных, необходимых для поиска элемента по его заголовку). тогда указы­ вайте и номер, и заголовок (например, /Art icle s/ 2392/AnnualReport). Таной URL дольше набирать , но он имеет больше смысла для человека и улучшает по­ исковый рейтинг. Приложение может просто игнорировать заголовок и отобра­ жать элемент, соответствующий идентификатору. • Не применяйте расширения имен файлов для НТМL-страниц (скажем, . aspx или .mv c ), но используйте их для специализированных типов файлов (таких как. jpg, .pdf и . zip). Веб-браузеры не придают значения расширениям имен файлов, если тип MIME установлен корректно, но люди ожидают, что имена файлов PDF заканчиваются на .pdf . • Со здавайте осмысленную иерархию (например. / Products /Menswear/ Shirts /Red), чтобы посетитель смог догадаться , юш выглядит URL родительской категории . • Поддерживайте независимость от регистра символов . (Кто-то может пожелать ввести URL. указанный на печатной странице.) По умолчанию система марш­ рутизации ASP.NET Core не чувствительна к регистру символов. • Избегайте применения знаков, кодов и символьных последовательностей. Если вам нужен разделитель слов , то используйте дефис (как в /my -great - article). Подчеркивания не являются дружественными к пользоват елю, а URL- кодированные пробелы выглядят либо странно (/my+great+article), либо пло­ хо (/my %20great %20art icle ). • Не изменяйте URL. Неработающие ссылки равнозначны потерям в бизнесе. После изменения URL продолжайте поддерживать старую схему URL насколько возможно долго посредством перенаправления . • Поддерживайте согласованность. Примите один формат URL в масштабах всего приложения. URL должны быть короткими, легкими для набора, реда~пируемыми человеком и постоянными, к тому же они обязаны отражать структуру сайта. Якоб Нильсен, гуру по удобству использования, развивает эту тему по адресу: https : //www .nngroup . com/articles/url-a s - ui/
Глава 16 . Дополнительные возможности маршрутизации 499 ТИ:м Бернерс-Ли, изобретатель веб, предлагает аналогичные советы по адресу: https : //www . wЗ . org/Provider/Style/URI GET и POST: выбор правильного запроса Эмпирическое правило гласит, что запросы GET должны применяться для извлече­ ния информации, предназначенной только для чтения, а запросы POS T - для любой операции, которая изменяет состояние приложения. В терминах соответствия стан­ дартам запросы GET предназначены для безопасных взаимодействий (не имеющих побочных эффектов помимо извлечения информации), а запросы POST - для небе­ зопасных взаимодействий (принятие решения либо изменение чего-либо). Такие со­ глашения установлены консорциумом WЗС (World Wide Web Consortium) и описаны по адресу: http : //www . w3 . o rg/Pr otoc o ls/rfc2616/rfc2 616- s ec9.html Запросы GET являются адресуемыми: вся информация содержится в URL, поэтому подобные адреса можно добавлять в закладки и ссылаться на них в будущем. Не используйте запросы GET для операций, которые изменяют состояние. Многие разработчики веб-приложений научились этому на горьком опыте в 2005 году, когда было выпущено приложение Google Web Accelerator. Оно с упреждением выбирало все содержимое, на которое ссылалась каждая страница, что в рамках протокола НТТР вполне законно . поскольку запросы GET должны быть безопасными. К сожалению, многие разработчики веб-приложений проигнорировали соглашения НТТР и разм е­ щали в своих приложениях простые ссылки на операции типа "удалить элемент" или "добавить в корзину" . В результате получился хаос. В одной компании были уверены , что их система управления содержимым стала целью повторяющихся атак злоумышленников, т.к . все содержимое постоянно удаля­ лось. Позже в компании обнаружили, что поисковый агент наткнулся на URL адми­ нистративной страницы и прошелся по всем ссылкам на удаление. Аутентификация может защитить от таких действий со стороны пользователей, но не способна обеспе­ чить защиту от веб-акселераторов. Резюме В этой глав е рассматривались дополнительные возможности системы маршру­ тизации. Вы узнали, как генерировать исходящие ссылки и URL, а также научились настраивать систему маршрутизации. По ходу дела вы ознакомились с концепцией областей и с рекомендациями относительно того, как должна создаваться удобная и содержательная схема URL . В следующей главе мы перейдем к исследованию контрол­ леров и действий, которые являются центральной частью инфраструктуры ASP.NET Core МVС. Будет подробно описана их работа и показано, как их применять для полу­ чения наилучших результатов в приложении.
ГЛАВА 17 Контроллеры и действия к аждый запрос, который поступает в приложение, обрабатывается контроллером. В инфраструктуре ASP.NEТ Core MVC контроллеры являются классами .NET, ко­ торые содержат логику, требуемую для обработки запроса. В главе 3 объяснялось, что роль контроллера заключается в инкапсуляции логики приложения. Это значит, что контроллеры отвечают за обработку входящих запросов, выполнение операций над мо­ делью предметной области и выбор представлений для визуализации пользователю. Контроллер волен обрабатывать запрос любым способом, какой посчитает подхо ­ дящим, до тех пор , пока он не забирается в области ответственности , которые от­ носятся к модели и представлению. Таким образом, контроллеры не содержат и не сохраняют данные, а также не генерируют пользовательские интерфейсы. В настоящей главе мы рассмотрим реализацию контроллеров, а танже различные способы применения контроллеров для получения и генерации вывода. В табл . 17 .1 приведена сводка, позволяющая поместить контроллеры в контекст. Таблица 17 .1 . Помещение контроллеров в контекст Вопрос Что это такое? Чем они полезны? Как ОНИ используются? Существуют ли какие-то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией MVC 5? Ответ Контроллеры содержат логику для получения запросов, обновления состояния или модели приложения и выбора ответа, который будет от­ правлен клиенту Контроллеры являются центральной частью проектов MVC и содержат логику предметной области для веб-приложения Контроллеры - это классы С#, открытые методы которых вызываются для обработки НТТР-запроса. Методы могут брать на себя ответствен ­ ность за выдачу ответа клиенту напрямую, но более распространенный подход предусматривает возвращение результата действия, который сообщает MVC, как ответ должен быть подготовлен Если вы - новичок в области разработки приложений MVC, то можете легко создавать контроллеры, содержащие функциональность, которая больше подходит для модели или представления. Более специфичная проблема связана с тем, что любые открытые классы с именами, закан­ чивающимися на Co n t r o ller, инфраструктура MVC считает контрол­ лерами; это означает возможность непредумышленной обработки НТТР­ запросов в классах, которые не планировались быть контроллерами Нет, контроллеры - это основная часть приложений MVC Способ определения и использования контроллеров был упрощен бла­ годаря объединению контроллеров Web API и обычных контроллеров (контроллеры API рассматриваются в главе 20). Вдобавок любой откры­ тый класс , имя которого заканчивается на Control l er , считается кон­ троллером , если только он не декорирован атрибутом NonController
Глава 17 . Контроллеры и действия 501 В табл. 17.2 приведен а сводка для этой главы. Таблица 17.2 . Сводка по главе Задача Определение контроллера Получение деталей НТТР-запроса Выдача результата из метода действия Выдача НТМL-результата Перенаправление клиента Возвращение содержимого клиенту Возвращение кода состояния НТТР Решение Создайте открытый класс с именем, заканчивающимся на Controller, или унаследуйте его от класса Contro l ler Используйте объекты контекста или определите параметры методов действий Работайте напрямую с объектом кон­ текста результата или создайте объект результата действия Создайте результат действия Создайте результат перенаправления Создайте результат содержимого Создайте НТТР-результат Подготовка проекта для примера Листинг 17 .1-17.10 17.11 -17.14 17 .15-17. 17 17. 18-17 .25 17.26-17 .31 17 .32-17 .36 17 .37-17.38 Для цел ей этой главы создайте новый проект типа Empty (Пустой) по имени ControllersAndActions с применением шаблона ASP.NET Core Web Applicatioп (.NET Core) (Веб - приложени е ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies файла proj ect . j son и настройте инструментарий Razor в разделе tools , как показ ано в листинге 17.1. Разделы, которые не нужны для данной главы, понадобится удалить. На заметку! Настоящая глава содержит модульные тесты для основных средств . Для крат­ кости проекты модульного тестирования в инструкции по созданию примера проекта не включены . Вы можете создавать тестовые проекты, следуя процессу, который описан в главе 7 , или загрузить код примеров из веб-сайта издательства. Листинг 17. 1 . Добавление пакетов в файле proj ect. j son " dependenc ies": { " Microsoft . NETCore . App ": "version": "1 .0 .0", " type ": " platform " }, "Microsof t.AspNetCore . Diagnostics ": " 1 . 0 .0 ", " Microsoft . AspNetCore . Server . IISintegration ": " 1.0 . 0 ", "Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ", " Microsoft . Extensions .L ogging.Console ": " 1 . 0 . 0 ", "Microsoft. AspNetCore. Mvc" : "1. О. О" , "Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 ,
502 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC }/ "Microsoft. AspNetCore. Session" : "1. О. О", "Microsoft.Extensions . Caching.Memory": 11 1.0.0 11 , "Microsoft . AspNetCore.Razor.Tools": { "version": "1. О. O-preview2-final", "type": "build" "t ools ": }' "Microsoft . AspNetCore.Razor . Tools": "l.0.0-preview2-final", "Microsoft.AspNet Core .Ser ver .IISintegrati on.Tools": "l.0 .0 -preview2-fi nal" " frameworks ": { " netcoreapp l. 0 ": " imports" : [ "dot net5 .6 ", "portaЫe-net45+win8" ] }' " b u ildOptions ": " emitEntr yPoint": t r ue , "preserveCompilationContext": t r ue В дополн ение к базовым пакетам MVC были добавлены пакеты, тр ебующиеся для хранилища сеанса. В листинге 17.2 приведен класс Startup . которы й конфигуриру ет приложение в соотв етствии с пакетами NuGet. Листинг 17.2 . Содержимое файла Startup. cs using Microsoft . As pNe t Core . Bui l der ; using Microsoft .E xtensions . Dependencyi njection ; namespace Co n trol l ersAndActi o n s puЫic class Startup { puЫic voi d ConfigureSer vi ces(ISer vi c eColl ection services) { services.AddМvc(); services.AddМemoryCache(); services.AddSession(); puЫic void Configu re(IApplicationBui lder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseSession(); app . UseMvcWithDefaultRoute(); Методы AddMernoryCach e ( ) и Add Ses s ion () создают службы , необходимые для управления сеансом. Метод UseSession ( ) добавляет в конвейер ко мпонент проме ­ жуточного ПО , который ассоциирует данные сеанса с запросами, и добавля ет cookie-
Глава 17 . Контроллеры и действия 503 наборы к ответам, чтобы гарантировать возможность идентификации будущих за­ просов. Метод UseSession () должен вызываться перед вызовом метода UseMvc (), так что компонент сеанса может перехватывать запросы, прежде чем они достигнут пром ежуточного ПО MVC , и модифицировать ответы после того, как они были сгене­ рированы. Друтие методы настраивают стандартные пакеты, которые рассматрива­ лись в глав е 14. Подготовка представлений Основное внимание в главе уделяется контроллерам и их методам действий , поэ­ тому классы контроллеров будут определяться в главе повсеместно. В качестве под­ готовки будут определены представления, которые помогут демонстрировать работу контроллеров и методов действий. Созданные в настоящем разделе представления находятся в папке Views/Sha r ed, что позволит их использовать в любых контрол­ лерах. которые будут определяться позже в главе. Создайте папку Views/Shared , до­ бавьте в нее файл представления Razor по имени Resul t . cshtml и поместите в файл разметку, показанную в листинге 17.3 . Листинг 17.З. Содержимое файла Resul t. cshtml из папки Views/Shared @model string @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name= " viewpo r t " content= " width=device - width " /> <title>Controllers and Ac tions</ tit le> <link rel= " stylesheet " asp-href- include= " lib/bootstrap/dist/css/* . min . css " /> </head> <body class= " panel -body " > Model Data: @Model </body> </html> Моделью для данного представления является объект string, который позволит отображать простые сообщения . Далее создайте в папке Views/Shared файл по име­ ни DictionaryResult . cshtml и поместите в него разметку, приведенную в листин­ ге 17.4 . В качестве модели для этого представления выбран словарь, который даст возможность отображать более сложные данные, чем в предыдущем представлении. Листинг 17.4. Содержимое файла DictionaryResult.cshtml из папки Views/Shared @model IDictionary<string , string> @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name= " v i ewport " content= " width=device - width " /> <title>Controllers and Actions</title> <link rel= " stylesheet " asp - href - include =" liЬ/ b ootstrap/dist/css/* . min . css " /> </head>
504 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Core MVC <body class= " panel -body " > <tаЫе class= " taЫe taЬle - bordered taЬle-condensed taЬle-striped"> <tr><th>Name</th><th>Value</th></tr> @foreach (string ke y in Model.Keys) { <tr><td>@key</td >< td>@Model[key]</td></tr> </tаЫе> </body> </html> Затем со здайте в папке Views/Shared файл по имени SimpleForm .cshtml и оп­ ределите в нем представл ение, показанное в листинге 17.5 . Как следует из названия, представление содержит простую НТМL- форму, которая будет получать данные от пользователя. Листинг 17.5. Содержимое файла SimpleForm. cshtml из папки Views/Shared @{ Layout = nul l; < !D OCTYPE html> <html > <head> <meta name= " viewport " content= " ~Jidth=device - width " /> <tit le >Controllers and Actions</title> <link rel=" stylesheet " asp-href-include= " liЬ/bootstrap/dist/ css/* .min. css" /> </head> <body class= " panel-body"> <form method= "post " asp - action= " ReceiveForm " > <div class= " form - group " > <label for= " name " >Name : </label > <input class= " form - control " name="name" /> </div> <div class= " form -g rou p" > <label for= "name " >City : </label> <input class = " form - control" name= " city" /> </div> <button class= "Ьt n Ьtn - primary center - Ыock " type= "submit " >Submit </button> </form> </body> </html> В представлениях используются встроенные дескрипторные вспомогательные классы для генерации URL из системы маршрутизации. Чтобы включить дескриптор­ ные вспомогательные классы, создайте в папке Views файл импортирования пред­ ставлений по имени _Viewlmports. cshtml и добавьте в него выражение . прив еден­ но е в листинге 17.6 . Листинг 17.6 . Содержимое файла_Viewimports. cshtml из папки Views @addTagHelper * , Microsoft . AspNetCore.Mvc. TagHelpers
Глава 17 . Контроллеры и д ейс твия 505 Все представления, созданные в папке Views/Shared, зависят от пакета CSS по имени Bootstrap . Для добавления Bootstrap в проект создайте файл bower . j son с применением шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте пакет Bootstrap (листинг 17.7) . Листинг 17.7 . Добавление пакета в файле bower. j son " name ": " asp.net", " private ": true , " dependencies ": { "bootstrap": 11 3.3.6 11 Понятие контроллеров Контроллеры - это классы С#, открытые методы которых (известные как методы дейсmвий или просто действия) отвечают за обработку НТТР-запроса и подготовку ответа, возвращаемого клиенту. Для определения, какой класс контроллера и метод действия должны обработать запрос, инфраструктура MVC использует систему мар­ шрутизации, описанную в главах 15 и 16. Затем она создает новый экземпляр класса контроллера , вызывает метод действия и применяет результат метода для выпуска ответа клиенту . Инфраструктура MVC снабжает методы действий данными контекста, так что они могут выяснить , как обрабатывать запрос. Доступен широкий спектр данных конт е кста, который описывает все, что касается текущего запроса , подготавливае­ мый ответ, данные, извлеченные системой маршрутизации, и детали учетных данных пользователя. Когда МVС вызывает метод действия, ответ из метода описывает ответ, который должен быть послан клиенту . С амый распространенный вид ответа создается путем визуализации представления Razor, поэтому метод действия использует свой ответ, чтобы сообщить MVC, какое представление задействовать и какие данные модели представления ему предоставить. Но имеются также другие виды ответов, и методы действий могут делать все, что угодно, от требования у MVC отправки клиенту пере­ направления НТГР до отправки сложных объектов данных . Таким образом, существуют три области функциональности , которые важны для понимания контроллеров. Во-первых, важно понимать особенности определения кон­ троллеров, так чтобы инфраструктура MVC могла применять их для обработки запро­ сов. Контроллеры представляют собой просто классы С#, но создавать их можно раз­ ными способами , и важно уяснить отличия между ними. Определение контроллеров рассматривается в разделе " Создание контроллеров" далее в главе. Во-вторых, важно понимать, каким образом МVС снабжает методы действий дан­ ными контекста. Получение необходимых данных контекста важно для эффективной разработки веб-приложений, а инфраструктура MVC облегчает его, определяя набор классов, которые используются с целью описания всего , что требуется тому или ино­ му методу действия. В разделе "Получение данных контекста" далее в главе объясня­ ется, как МVС описывает запросы и ответы .
506 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Наконец, в-третьих, важно понимать, каким образом методы действий выпускают ответ. Методы действий редко нуждаются в самостоятельной генерации НlТР-ответа, и вы должны знать, как инструктировать MVC о необходимости выпуска требуемых ответов, что будет показано в разделе "Генерирование ответа" позже в главе. Создание контроллеров Вы сталкивались с применением контроллеров почти во всех предшествующих главах. Наступило время заглянуть "за кулисы" и посмотреть, как они определяются. В последующих разделах будут описаны различные способы создания контроллеров и приведены объяснения отличий между ними. Создание контроллеров РОСО Инфраструктура MVC поддерживает соглашение по конфигурации, а это значит, что контроллеры в приложении MVC обнаруживаются автоматически вместо того, чтобы определяться в конфигурационном файле. Базовый процесс обн аружения прост: любой класс puЫic с именем, заканчивающимся на Con t roller, является контроллером, а любой определенный в нем метод puЫic - действием. Чтобы вы­ яснить, как это работает, добавьте в проект папку Controllers и поместите в нее файл класса по имени PocoController . cs, содержимое которого приведено в лис­ тинге 17.8 . Совет. Хотя соглашение предусматривает помещение контроллеров в папку Controllers, они могут находиться где угодно в проекте и MVC по-прежнему отыщет их. Листинг 17.8 . Содержимое файла PocoController. cs из папки Controllers namespace ControllersAndActions . Controll ers puЬlic class PocoController { puЬlic string Index{ ) => " This is а РОСО controller"; Класс PocoController удовлетворяет простому критерию, который МVС ожидает найти в контроллере. Он определяет единственный метод puЫic по имени Index () , который будет использоваться как метод действия и который возвращает объект string. Класс PocoController является примером контроллера РОСО, где РОСО озна­ чает "plain old CLR object" ("простой старый объект CLR") и опирается на тот факт, что контроллер реализован с применением .стандартных средств .NET без какой-либо прямой зависимости от АРI-интерфейса, предоставляемого инфраструктурой ASP.NET Саге МVС. Чтобы протестировать контроллер РОСО, запустите приложение и запросите URL вида /Poco/Index/. Система маршрутизации будет сопоставлять запрос со стандар­ тным шаблоном URL и направлять запрос методу Index () класса PocoController (рис. 17. 1) .
Глава 17. Контроллеры и действия 507 ~ С [ ф localhost:61521/Poco/lndex This i s а РОСО controller Рис. 17. 1. Использование контроллера РОСО Применение атрибутов для корректировки идентификации контроллеров Поддержка контроллеров РОСО не всегда работает желательным образом . Общая проблема заключается в том, что MVC будет идентифицировать в качестве контроллеров фиктивные классы, созданные для модульного тестирования. Избежать указанной проблемы проще всего, уделяя внимание именам классов и не допуская имен вроде FakeCont r o ll er . Если это невозможно, тогда применяйте к классу атрибут Non Co n t r o lle r, определенный в пространстве имен Mi crosoft . AspNe t Co re. Mvc , чтобы сообщить MVC о том, что класс не является контроллером . Имеется также атрибут No nAc tio n, который может при­ меняться к методам , чтобы предотвратить их использование как методов действий. В ряде проектов может отсутствовать возможность следования соглашению об именовании для класса, который должен выступать в качестве контроллера РОСО. Инфраструктуре MVC можно указать, что класс является контроллером, даже когда он не удовлетворяет критерию выбора РОСО, применив атрибут Cont roller, который также определен в пространстве имен Microso ft. AspNetCore . Mv c. Использование АРl-интерфейса контроллеров инфраструктуры MVC Класс PocoCont rol l er - это удобная демонстрация способа идентификации контроллеров инфраструктурой MVC и возможной реализации простых контролле­ ров. Но чuсmы.е контроллеры РОСО. не имеющие зависимостей от пространств имен Mi crosoft . As pnetCore , не особенно полезны. поскольку у них отсутствует доступ к средствам , которые MVC предлагает для обработки запросов . Доступ к определенным частям АРI-интерфейса МVС может осуществляться за счет создания новых экземпляров классов из пространств имен Microsoft . Aspn etCore. В качестве простого примера класс РОСО может затребовать у MVC визуализацию представления Razor, возвращая объект Vi ewRes ul t и з своих методов действий , как показано в листинге 17 .9 . (Класс Vi e wRes ul t будет рассматриваться в разделе "Генерирование отв ета" далее в главе.) Листинг 17.9 . Использование АРl-интерфейса MVC в файле PocoController. cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Cont r ollersAndAct i ons.Cont r oller s puЫic cl ass PocoControlle r {
508 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC puЫic ViewResul t Index () => new ViewResul t () ViewName = "Result", ViewData = new ViewDataDictionary ( new EmptyModelMetadataProvider(), new ModelStateDictionary()) { }; Model = $ 11 This is а РОСО controller" } Класс PocoCont r o ller больше не является чистым контроллером РОСО. т.к . он имеет прямые зависимости от АРI-интерфейса MVC . Однако, оставив в стороне чисто­ ту, данный класс намного более полезен, чем класс из предыдущего примера. потому что он запрашивает у MVC визуализацию представления Razor. К сожалению, код ока­ зывается сложным . Чтобы создать объект Vi e wRes u l t, необходимо создать объекты Vi ewDataDi cti onary , EmptyModelMetadat a Pr ovide r и Mode l StateDic t ionary, которые требуют доступа к трем разным пространствам имен. (Средства, к которым относятся эти типы. будут описаны в последующих главах . ) Суть рассматриваемого примера - демонстрация того, что к средствам, предлагаемым MVC, можно обра­ щаться напрямую, даже если в результате получается некоторая путаница. Выделенные полужирным изменения в листинге 17.9 обеспечивают визуализацию представления Re s u l t . cshtml с применением типа string в качестве модели пред­ ставления. Запустив приложение и запросив URL вида /Poco/Index , вы получите ответ, приведенный на рис . 17.2 . f- С <D localhost :61521/Pocoilпdex Model Oata: Тhis is а РОСО co ntroller Рис. 17.2 . Работа с АРl-интерфейсом MVC напрямую Использование базового класса Controller Пр едшествующие примеры проиллюстрировали, каким образом начать с контрол­ лера РОСО и обеспечить ему доступ к средствам МVС. Такой подход проливает свет на то, как работает MVC; это полезное знание на тот случай, если вы обнаружите, что невольно создаете контроллеры, но контроллеры РОСО неудобны в написании, восприятии и сопровождении . Более легкий способ создания контроллеров предусматривает наследование клас­ сов от класса Mic r o s oft . AspNetCore . Mvc . Cont r oll er, в котором определены ме­ тоды и свойства, предоставляющие доступ к средствам MVC в сжатой и удобной ма ­ нере. Добавьте в папку Con t rolle r s файл класса по имени DerivedCon t r ol l er . cs с определением контроллера, приведенным в листинге 17. 1О .
Глава 17 . Контроллеры и действия 509 Листинг 17.10. Наследование от класса Controller в файле DerivedController. cs из папки Controllers using Microsoft . AspNetCore . Mvc ; namespace ControllersAndActions.Controllers puЬlic class DerivedController : Controller { puЫic ViewResult Index() => View(HResult", $"This is а derived controller"); Запустив приложение и запросив URL вида / Der i ved/ Index, вы получите резуль­ тат, показанный на рис. 17.3. 1 Model Data: This is а derived controller L--------------------------- Рис. 17.3. Использование базового класса Controller Контроллер в листинге 17. 1О делает то же самое, что и контроллер в листинге 17.9 (он требует у инфраструктуры MVC визуализировать представление с моделью пред­ ставления string), но применение базового класса Controller означает возмож­ ность получения результата более простым путем. Основное изменение касается того, что объект ViewResul t, т ребующийся для визуализации представления Razor, можно создать с использованием метода View () вместо явного создания экземпляра этого класса (и необходимых ему других типов) прямо в методе действия. Метод View () унаследован от базового класса Controller, а объект ViewResul t по-прежнему создается тем же способом , просто не загромождая метод действия. Наследование от класса Controller не изменяет манеру работы конт­ роллеров; оно всего лишь упрощает код, который пишется для решения общих задач. На заметку! Инфраструктура MVC создает новый экземпляр класса контроллера для каждого запроса, который ей предлагается обработать. Это значит, что вам не придется синхро­ низировать доступ к методам действий или свойствам и полям экземпляра. Разделяемые объекты, в том числе базы данных и службы-одиночки, которые описаны в главе 18, могут применяться параллельно и должны быть соответствующим образом реализованы. Получение данных контекста Независимо от того, как определяются контроллеры, они редко будут существовать в изоляции и обычно нуждаются в доступе к данным из входящего запроса, таким как значения строки запроса, значения формы и параметры, извлеченные из URL
51 О Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC системой маршрутизации, что все вместе называют данными контекста. Есть три основных способа досrупа к данным контекста : • извлечение данных из набора объектов конте1сста; • получение данных как параметра метода действия; • явное обращение к средству привязки моделей инфраструктуры . Здесь мы рассмотрим подходы к получению входных данных для м етодов дейс­ твий, сосредоточившись на объектах контекста и параметрах методов действий . Привязка моделей р а скрывается в глав е 26. Получение данных из объектов контекста Одно из главных преимуществ использования базового класса Co n tro ller для создания контроллеров заключа ется в удобном досrупе к набору объ ектов контекста, которые описывают текущий запрос, подготавливаемый ответ и состояние приложе­ ния. В табл. 17.3 описаны наиболее полезные свойства контекста Contr o l ler. Таблица 17.З. Полезные свойства класса Controller для данных контекста Имя Reque s t Respo ns e HttpCon text RouteData Mo d elState User Описание Это свойство возвращает объект HttpRe que s t , который описывает запрос, полученный от клиента (табл. 17.4) Это свойство возвращает объект HttpResponse, который применя­ ется для создания ответа клиенту (табл . 17.7) Это свойство возвращает объект Ht tpCo n text , который является источником многих объектов , возвращаемых другими свойствами , такими как Re que s t и Re s ponse . Оно также предоставляет инфор­ мацию о доступных возможностях НТТР и доступ к низкоуровневым средствам наподобие веб-сокетов Это свойство возвращает объект RouteData, выпускаемый сис­ темой маршрутизации, когда имеется совпадение с запросом , как описано в главах 15 и 16 Это свойство возвращает объект Mo d elSta t eDictionary, кото­ рый используется для проверки достоверности данных, отправлен­ ных клиентом (глава 27) Это свойство возвращает объект ClaimsPrincipal, который опи­ сывает пользователя, сделавшего запрос, как объясняется в глава х 29и30 Многие контроллеры пишутся без применения свойств из табл. 17 .3 , потому что данные контекста доступны через средства, рассматриваемые в последующих главах, которые больше гармонируют со стилем разработки приложений MVC. Напри м ер , большинство контроллеров не нуждаются в использовании свойства Request для по­ лучения деталей обрабатываемого НТТР-запроса, поскольку та же самая информация доступна через процесс привязки модели, который обсуждается в главе 26. Но свойства, перечисленные в табл. 17.3, все же могут быть полезны для понима­ ния и работы с объектами контекста, а также для целей отладки . В листинге 17 .11 свойство Requ es t применяется для досrупа к заголовкам в НТТР-запросе .
Глава 17 . Контроллеры и де й ствия 511 Листинг 17 .11 . Использование данных контекста в файле DerivedCon troller. cs using Microsoft . AspNetCore . Mv c ; using System.Linq; namespace Control l ersAndActions . Controllers puЬlic c l ass DerivedController : Controller { puЬlic ViewResult Index(} => View( " Result ", $ " Thi s is а derived controller " } ; puЬlic ViewResul t Headers () => View ( "DictionaryResult" , Request.Headers.ToDictionary(kvp => kvp.Ke y, kvp => kvp.Value.First())); Работа с объектами контекста означает перемещени е по диапазону различных типов и простран ств имен. Свойство Contr o ller. Request , которо е применяет ­ ся для получ ения данных контекста о НТГР-запросе в листинг е , во звраща ет объект HttpRequest . В табл . 17 .4 описаны свойства HttpRequest, наибол ее полезные при напи сан ии классов контроллеров. Таблица 17.4. Распространенные свойст ва HttpRequest Имя Path QueryString Headers Body Form Cookies Описание Это свойство возвращает раздел пути URL запроса Это свойство возвращает раздел строки запроса URL запроса Это свойство возвращает словарь заголовков запроса, индексированный по именам Это свойство возвращает поток, который может использоваться для чте­ ния тела запроса Это свойство возвращает словарь данных формы в запросе, индексиро­ ванный по именам Это свойство возвращает словарь сооkiе-наборов запроса, индексиро­ ванный по именам Свойство Request . Headers прим еня ется для получ ен ия слов аря заголовков, ко­ торый обрабатывается с помощью LINQ: View( " DictionaryResult ", Request . Headers .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.First() }} ; Словарь , возвращаемы й свойством Request . Headers, хр анит зна ч е ния всех з аго­ ловков с исполь зованием структуры StringValues, которая при ме ня ется в ASP.NET для пр едставления последовательности строковых знач ени й . Клиент НТТР может от­ править для заголовков НТГР несколько значений, но нам нужно отобр азить только п е рво е з нач е ние. С помощью LINQ-м eтoдa ToDictionary () для каждого заголовка получается объ ект KeyValuePair<string , Str i ngValues > и выбирает ся первое значение . Результатом явля ется словарь , содержащий значения string , которые мо­ гут отображать ся пр едставлени ем DictionaryResult.
512 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC Запустив приложение и запросив URL вида /De ri v e d/Head er s , вы получите вы­ вод , подобный показанному на рис. l 7.4. (Набор заголовков и их значений будет от­ личаться в зависимости от используемого браузера.) [j Contror~s and Actioru; )( ~- --~ [~~~~~~:~~~:_~~~~.~~-i~~=~:.~~~=~=;:;-:,-_~-=~~- Name Conn ection Accep t Accep t- Encoding Ассер\- Language Host Val ue Kee p-Alive =~::,::::::"""'""'mlopPl"'loo'"" '"''""""'~ь..·r'"'' 1 en·US .e n;q:O.B 1 locall10st:61521 User-Agent Mozilla/5.0 (Windows NT 10 .О; W0\"164) AppleWebKiV537.36 (KHTML. j .г- ~k~"k~,~~~~ ~4'""' Рис. 17 .4. Отображение данных контекста получение данных контекста в контроллере РОСО Хотя контроллеры РОСО не особенно полезны в обычных проектах, они позволя­ ют заглянуть "за кулисы" и посмотреть, как функционирует МVС. Получение данных контекст а в контроллер е РОСО свя з ано с проблем ой , поскольку нельзя про сто со здать собственные объекты Ht tpReque s t или Http Respons e ; необходимы объекты , кото­ рые были созданы ASP.NET и обновлены всеми компонентами промежуточного ПО, заполняющими поля данных этих объектов по мере обработки запроса. Чтобы получить данные контекста, контроллер РОСО должен запрос ить их пре­ доставление у инфраструктуры МVС . В листинге 17 .12 класс PocoCon troll e r моди­ фицирован для добавл ения метода действия, который отображает заголовки НТГР­ запроса. Листинг 17.12. Отображение данных контекста в файле PocoController. cs u sing Microsoft . AspNetCore . Mvc ; using Mi c r osof t .AspNetCor e . Mvc .ModelBinding ; using Microsoft.AspNetCor e .Mvc .Vi ewFeatures; using System.Linq; names p ace Control le r sAndActio ns. Cont r ol l ers puЫic cl ass PocoController { [ControllerContext] puЫic ControllerContext ControllerContext { get; set; } puЫic Vi ewResult Index (} => new ViewResult(} ( }; ViewName "Result", Vi ewDa t a = n ew ViewDa t aDic ti o n ary( n ew EmptyMode l MetadataProvider(} , new ModelStat eDictionar y(} } ( Model = $ "This is а РОСО controll er"
Глава 17. Контроллеры и действия 513 puЫic ViewResult Headers () => new ViewResult() { }; ViewName = "DictionaryResult", ViewData = new ViewDataDictionary( new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = ControllerContext.HttpContext.Request.Headers .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.First()) Дllя получения данных контекста определяется свойство по имени Con trollerContext типа ControllerContex t, которое декорировано атрибутом, также имеющим имя Control l erContext . Пол езно объяснить три разных случая применения термина ControllerContext . Первое использование - класс ControllerContext из пространства имен Microsoft . AspNetCore . Mvc, представляющий собой класс, который собирает вмес­ те все объекты контекста, требующиеся методу действия контроллера, с применением свойств, описанных в табл. 17.5 . Таблица 17.5. Самые важные свойства ControllerContext Имя Act i onDesc riptor Ht tpContext ModelState Ro uteData Описание Это свойство возвращает объект ActionDescriptor, который описывает метод действия Это свойство возвращает объект HttpContext, который предо­ ставляет детали НТТР-запроса и отправляемого взамен НТТР-ответа (табл. 17.6) Это свойство возвращает объект Mode lStateDictionary, кото­ рый используется для провер ки достоверности данных , отправляе­ мых клиенту (глава 27) Это свойство возвращает объект RouteData, описывающий способ, которым система маршрутизации обработала запрос (глава 15) Дос туп к данным, связанным с НТТР, осуществляется через свойство Cont r o ll e rC ontext . HttpCo n text , которое возвращает объект Microsoft . AspNetCore . Http . HttpCon text . Класс HttpContext объединяет несколько объек­ тов, описывающих различные аспекты запроса, которые доступны через свойства, перечисленные в табл . 17.6 . Второе использование атрибута Con tro llerContext прод емонстри ровано в лис­ тинге 17.12: с его помощью декорируется свойство, чтобы сообщить инфраструктуре МVС о необходимости установки значения свойства в объект Contro llerCo ntext, который описьmает текущий запрос. В этом случае применяется прием, назьmаемый внедрением зависимостей, который рассматривается в главе 18, и MVC будет исполь­ зовать такое свойство для предоставления контроллеру данных контекста перед вы­ зовом какого-нибудь метода действия, чтобы обработать запрос.
514 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Таблица 17.6. Распространенные свойства HttpContext Имя Connection Request Response Session User Описание Это свойство возвращает объект Connectioninfo, который описывает низ коуровневое подключение к клиенту Это свойство возвращает объект HttpRequest, который описывает НТТР-запрос, полученный от клиента, как объяснялось ранее в главе Это свойство возвращает объект HttpResponse, применяемый для со­ здания ответа, который будет возвращен клиенту, как описано в разделе " Генерирование ответа" далее в главе Это свойство возвращает объект ISession, описывающий сеанс, с которым ассоциирован запрос Это свойство возвращает объект ClaimsPrincipal, который описыва­ ет пользователя, ассоциированного с запросом (глава 28) Наконец , третье прим енение термина ControllerContext - имя самого свойс­ тва. В контроллерах РОСО можно использовать любые допустимые имена свойств С#, но имя ControllerContext было выбрано из-за того, что оно прим еняется классом Controller. "За кулисами" класс Controller для своих данных контекста полага­ ется на тот же самый класс ControllerContext, которы й декорирован тем же са­ мым атрибутом ControllerContext . Все свойства класса Controller, описанные в табл. 17.3, являются просто более удобной и краткой альтернативой использованию свойств класса ControllerContext напрямую, что в действительности происходит внутри свойств, предоставляемых классом Controller. В качестве примера ниже приведено определение свойства HttpContext из класса Controller: puЫic HttpContext HttpContext { get { return ControllerContext . HttpContext ; Свойство HttpContext - это всего лишь более удобный способ получить значе ­ ние свойства ControllerContext . HttpContext. Никакой "магии" в базовом классе Controller нет: он дает в результате более простые и ясны е контроллеры по той при­ чине, что объединяет общие задачи в удобные методы и свойства, которые при необ­ ходимости можно воссоздать самостоятельно в контроллере РОСО . Если углубиться в детали, то выяснится, что масса функциональности в инфраструктуре ASP.NEТ Core MVC удивительно проста; здесь отсутствует какая-либо специальная изюминка - только продуманная функциональность , предлагаемая тщательно спроектированным набором пакетов NuGet . При наличии времени рекомендуется удостовериться в это м, загрузив исходный код МVС из http : //github . com/aspnet и изучив его. Использование параметров метода действия Определенные данны е контекста могут быть также получ ены через пара метры метода действия, что позволяет создавать более естественный и элегантный код. Распростран енный приме р касается ситуации , когда метод действия нуждается в по­ лучении значений данных формы, отправленной пользователем. Для сравнения далее
Глава 17. Контроллеры и де йс твия 515 будет показано, как получить данные формы через объекты контекста и затем через параметры метода действия. Доступ к значениям данных формы осуществляется посредством свойс­ тва Request . Forrn класса Controller. В целях демонстрации добавьте в папку Controllers файл класса по имени HomeController. cs и определите в нем произ­ водный контроллер, приведенный в листинге 17.13. Листинг 17.13. Содержимое файла HomeController. cs из папки Controllers using Mic rosoft.AspNetCore.Mvc; namespace ControllersAndActions.Controllers puЬlic c la ss HorneController : Con troller { puЫic Vi ewRes ult Index() = > View( "Sirnpl eForrn " ) ; puЫic ViewRes ult ReceiveForrn() { var name = Request . Form["narne "]; var city = Request . Form[" city"] ; return View("Result ", $ " {name} lives in {city}"); Метод действия Index () в этом контроллере визуализирует представление SimpleForm, которое было создано внутри папки Views/Shared в начале главы. Нас интересует метод Recei veForm (), потому что он применяет объект контекста HttpRequest для получения значений данных формы из запроса. Как бьmо описано в табл. 17.4 , свойство Forrn, определенное в классе HttpRequest, возвращает коллекцию значений данных формы, индексированную по именам ассоци­ ированных НТМL-элементов. В представлении SimpleForm присутствуют два элемента inpu t , narne и city, значения которых извлекаются из объекта контекста и использу­ ются для создания строки, передаваемой представлению Resul t в качестве его модели. После запуска приложения и запроса URL вида /Ноте отобразится форма. Когда вы заполните поля и щелкнете на кнопке Submit (Отправить), браузер отправит данные формы как часть НТГР-запроса POST, который будет обработан методом Recei veForm (), порождая показанный на рис. 17 .5 результат. Name: _ -~---?-~~~~~~~~'e1.R.:~eN~Fo_rn1 ---~- J Model Data: Adam lives in London 1 ~------"·--г-··--------·-------~ ' Adam City: Гlond;~ -~---- ------------"·--11 ___________ · _____ _ _____ _! Рис. 17 .5 . Получение данных формы из объектов контекста
516 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Проиллюстрированный в листинге 17. 13 подход работает велюилепно, но су­ ществует более элегантная альтернатива. В методах действий можно определять параметры, которые MVC применяет для передачи данных контекста контроллеру, в:ключая детали НТТР-запроса. Альтернативный подход более ла:коничен, чем из­ влечение данных :конте:кста непосредственно из объектов конте:кста, и дает методы действий. которые легче в восприятии. Чтобы получить данные формы, объявите в методе действия параметры, имена :которых соответствуют значениям данных формы (листинг 17.14). Листинг 17.14. Получение данных контекста как параметров в файле HomeController. cs using Microsoft .As pNetCore . Mvc ; namespace ControllersAndActions . Controllers puЬlic class HomeControl l er : Contro ll e r { puЬlic ViewR e sul t Index () => View ( "S impleForm " ) ; puЫic ViewResult Rece i veForm(str i ng name , string c i ty) => View( " Result ", $ " {name} lives in {city} " ) ; Пересмотренный метод действия выпускает тот же самый результат, но он про­ ще в восприятии . Инфраструктура MVC будет предоставлять значения для пара­ метров метода действия , автоматически проверяя объе:кты конте:кста, в том числе Request . QueryString , Request . Form и RouteData. Va lu es . Имена параметров трактуются :ка:к нечувствительные :к регистру символов, таи что параметр метода действия ci ty может быть наполнен значением из Request . Form [ "City " ] , напри­ мер. Такой подход также дает методы действий, легче подцающиеся модульному тес­ тированию, пос:коль:ку значения , которыми оперирует метод действия , получаются :как обычные параметры С# и не требуют имитации объектов контекста. Генерирование ответа После того как метод действия завершил обработку запроса , ему необходимо сге ­ нерировать ответ. Для генерирования вывода из методов действий доступно много средств, которые будут описаны в последующих разделах. Генерирование ответа с использованием объекта контекста Самый низкоуровневый способ генерации вывода предусматривает применение объекта контекста HttpResponse - именно так инфраструктура ASP.NET Core пре­ доставляет доступ к НТТР-ответу, который будет отправлен клиенту . В табл. 17. 7 пе­ речислены базовые свойства класса HttpRespon se , определенного в пространстве имен Microsoft . AspNetCore . Http. В листинге 17.15 контроллер Ноте обновлен, чтобы его действие Recei vedForm генерировало ответ с использованием объекта HttpResponse, возвращаемого свойс­ твом Controller. Request.
Глава 17. Контроллеры и действия 517 Таб лица 17.7 . Распространенные свойства HttpResponse Имя Описание StatusCode Это свойство используется для установ к и кода состояния НТТР, связанного с ответом ContentType Это свойство применяется для установки заголовка Content-Type ответа Headers Это свойство возвращает словарь заголов ков НТТР, которые будут включе­ ны в ответ Cookies Body Это свойство возвращает коллекцию, которая используется для добавле­ ния сооkiе-наборов к ответу Это свойство возвращает объект System . IO . Stream, который применя­ ется для записи данны х тела запроса Ли стинг 17.15. Выпуск ответа в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using System.Text; namespace ControllersAndActions.Controllers puЫic c l ass HomeController : Controller { puЫic ViewResu l t Index () => View ( " SimpleForm " ) ; puЬlic void ReceiveForm(string name, string city) Response.StatusCode = 200; Response.ContentType = "text/html"; byte [] content = Encoding .ASCII . GetBytes ($"<htmJ.><body>{name} li.ves i.n {ci.ty}</body>"); Response.Body.WriteAsync(content, О, content.Length); Это плохой способ генерации ответа из-за жесткого кодирования НТМL-разметки в методе действия с применением строк С# , что чревато ошибками и трудно поддается модульному тестированию . Тем не менее, мы получаем отправную точку для выясне­ ния , каким образом ответы создаются "за кулисами". Сушествуют эффективные альтернативы, которые пр едусматривают работу непос­ редственно с объектом HttpResponse . Инфраструнтура MVC строит низкоуровневый ответ с помощью намно го более удобного средства, находящегося в центральной час­ ти функциональности контроллеров - результата действия. Понятие результатов действий Результаты действий используются в МVС для отделения заявлений о намерениях от выполнения намерений. После освоения концепция покажется простой, но понима­ ние лежащего в основе по дхода может занять не1юторое время по причине присушей ему косвенности . Вместо того чтобы иметь дело напрямую с объектом HttpResponse . методы дейс­ твий возвращают объект, который реализует интерфейс IActionResul t из про­ странства имен Microsoft . AspNetCore . Mvc. Объе кт реализации IActionResu l t,
518 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC известный как результат действия, описывает, каким должен быть ответ из конт­ роллера, например, визуализация представления или перенаправление клиента на другой URL . Но - и здесь в игру вступает косвенность - ответ напрямую не генери­ руется. Взамен для выпуска результата инфраструктур а MVC обрабатывает результат действия. На заметку! Система результатов действий является примером паттерна Команда. Этот паттерн затрагивает сценарии, при которых сохра няются и передаются объе кты, описы­ вающие выполняе м ые операции . Дополнительные детали можно найти в книге Роберта Мартина Гибкая разработка программ на Java и С++ : принципы, паттерны и методики (Диалектика, 2016 год). Ниже приведено определение интерфейса IActionResul t из исходного кода МVС: using System . Threading . Tasks ; namespace Microsoft . AspNetCore . Mvc puЫic interface IActionResult { Task ExecuteResultAsync(ActionContext context) ; Интерфейс IActionResult может показаться простым, но это потому, что MVC не навязывает виды ответов, которые может выпускать результат действия. Когда метод действия возвращает результат действия. инфраструктура МVС вызывает его метод ExecuteResul tAsync (),который отвечает за генерирование ответа от имени метода действия. Аргумент ActionContext пр едоставляет данные контекста для генерации ответа, включая объект HttpResponse . (Клас с ActionContext является суперклас ­ сом coritrollerContext и определяет все свойства. описанные в табл. 17.5 .) Чтобы по смотреть, как работают результаты действий , добавьте в проект папку Infrastructure и поместите в нее файл класса по имени CustomHtmlResult . cs с определением результата действия из листинга 17 .16. Листинг 17.16. Содержимое файла CustomHtmlRes ult.cs из папки Infrastructure using Microsoft . AspNetCore .M vc ; using System .Te xt ; using System .T hreading .T asks ; namespace ControllersAndActions . Infrastructure puЫic c lass CustomHtmlResu lt : IActionResult puЫic string Conte nt { get; set ; } puЫic Task ExecuteResultAsync(ActionContext context) { context . HttpContext . Response . StatusCode = 200 ; context.Ht tpC ontext.Response . ContentType = " text/html "; byte (] content = Encoding . ASCII . GetBytes(Content) ; return context .HttpContext . Response .Body. Wr iteAsync(content , О , content . Length);
Глава 17. Контроллеры и действия 519 Класс CustomHtmlResul t реализует интерфейс IActionResul t, а его метод ExecuteResultAsync () применяет объект HttpResponse для записи НТМL-ответа, ко­ торый содержит значение свойства по имени Content. Метод ExecuteResul tAsync () должен возвращать объект Task, так что ответ может строиться аси нхронно. Это хорошо согласуется с его реализацией в классе CustomHtmlResul t, которая полага­ ется на метод WriteAsync () объекта Stream, представляющего тело ответа. Вызов WriteAsync () возвращает объект Task, который можно использовать в каче стве ре­ зультата метода ExecuteResul tAsync () в классе CustomHtmlResul t . В листинге 17.17 класс результата действия при мен яется к контроллеру Home, уп­ рощая метод действия Recei veForm () контроллера Home. Листинг 17.17. Использование результата действия в файле HorneController.cs using Microsoft . AspNetCore.Mvc ; using System . Text ; using ControllersAnc:!Actions.Infrastructure; namespace ControllersAndActions . Controllers puЫic class HomeController : Control l er { puЫic ViewResul t Index () => View ( " SimpleForm " ) ; puЬlic IActionResult ReceiveForrn(string narne, string city) => new CustornНtrnlResul t { Content = $ 11 {narne} lives in {city}" }; Код, отправляющий ответ, теперь определен отдельно от данных, которые содер­ жатся в ответе, что делает метод действия проще и позволяет создавать ответ того же самого типа в других методах действий, не дублируя код. Модульное тестирование контроллеров и действий Многие части ASP.NET Core MVC спроектированы для упрощения модульного тестирования, и это особенно справедливо в отношении действий и контроллеров. Наличие такой подде­ ржки объясняется несколькими причинами. • Тестировать действия и контроллеры можно за пределами веб-сервера . • Для тестирования результата метода действия проводить разбор какой-либо НТМL­ разметки не понадобится. Чтобы удостовериться в получении ожидаемых результатов, можно проинспектировать возвращаемый объект реализации IActionResul t . • Эмуляция клиентских запросов не нужна . Система привязки моделей MVC позволяет пи­ сать методы де й ствий, которые получают входные данные в качестве своих параметров . Для тестирования метода действия необходимо просто вызвать его напрямую и предо­ ставить интересующие значения параметров. Далее в главе будет показано, как создавать модульные тесты для разных видов результа­ тов действий. В главе 7 приводились инструкции для настройки проекта модульного тести­ рования; можно также загрузить готовые проекты из веб - сайта издательства.
520 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Генерирование НТМL-ответа В предыдущем разделе была возможность изъять из класса контроллера код. ге­ нерирующий ответ, с прим енением результата действия . Инфраструктура ASP.NET Core MVC укомплектована более гибким подходом 1t выпуску ответов - классом ViewResul t . Класс Vi ewResul t - это результат действия, предоставляющий доступ к механиз­ му визуализации Razor, который обрабатывает файлы . cshtrnl для внедрения данных модели и отправляет результат клиенту через механизм контекста HttpResponse. Работа механизмов визуализации объясняется в главе 21, а здесь мы сосредоточим внимание на использовании класса ViewResult как результата действия. В листинге 17 .18 специальный класс результата действия заменен классом ViewResul t, экземпляр 1шторого создается посредством метода View (), предостав­ ляемого базовым классом Controller . Листинг 17.18. Применение класса ViewResul t в файле HomeController. cs using Microsoft.AspNetCore .Mv c ; using System . Text; using Cont roll ersAndActions .Infras tructure; name s pace ControllersAndAct ions . Con tr o lle rs puЫic class HomeController : Controller { puЫic ViewResult Index() => Vi ew (" SirnpleForm"); puЬlic Vi e wResu l t Receive Forrn(string narne, string c ity ) => View("Result ", $ "{name} lives in {city}") ; Объекты ViewResul t можно создавать напрямую, как демонстрировалось в кон­ троллере РОСО в начале главы, но использование метода View () дает более простой и краткий код. Класс Controlle r предлагает несколько версий метода View (), кото­ рые позволяют выбирать представление, подлежащее визуализации, и снабжать его данными модели (табл. 17.8). Таблица 17.8. Методы View () класса Controller Метод Описание View () Этот метод создает объект ViewRes ul t для стандартного представ­ ления, ассоциированного с методом действия, так что вызов View () в методе по имени MyAction () будет визуализировать представление под названием MyAction. cshtrnl. Данные модели не применяются View (vi ew) Этот метод создает объект ViewResul t, который визуализирует указанное представление, так что вызов View ( " MyView ") будет визуализировать представление по имени MyView . cs h trnl . Данные модели не используются View (rnodel) Этот метод создает объект ViewResul t для стандартного пред­ ставления, ассоциированного с методом действия, и применяет указанный объект как данные модели View (vi ew , rnodel) Этот метод создает объект Vi ewResul t для указанного представле­ ния и использует указанный объект как данные модели
Глава 17. Контроллеры и действия 521 Запустив приложение и отправив форму, вы увидите знакомый результат (рис. 17.6) . D Control lers ~nd Action< Х 1f- С) ControUers ond Actio11> Х , 1 1 Name: ---·------------·--··--- ,---- . -----·-- ·-------··- __ _ _ _ _ "]. ~ ; С [so_~-~lho~~;_::_:1rм~1_:~~eceiveF~-':'2J ; i Adam! -- -·-" ---- - ·----- __"_ -- ---- ·-- --· ". -- - - " -- -·· / ··--- - -- ---- --- --- --- . Model Data : Adam lives in London j City: [-------~------------- ! Londo11 1 1L ____ _ "'' J ----- ·--~·-------·- Рис. 17.6 . Применение объекта ViewResul t для генерации НТМL-ответа Поиск файла представления Когда инфраструктура MVC вызывает метод ExecuteResul tAsync () объ­ екта ViewResul t, начинается поиск представления, которое было указано. Последовательность каталогов, где МVС ищет представление , является примером со­ глашения по конфигурации. Вам не придется регистрировать файлы представлений с помощью инфраструктуры . Вы просто помещаете их в одно из набора известных мес­ тоположений, и инфраструктура найдет их. По умолчанию MVC будет искать пред­ ставление в следующих местоположениях: /Vi еws /<ИмяКонтроллера>/<ИмяПредставления> . сshtml /Vi ews /Shared/<ИмяПpeдcтaвлeния> .cshtrnl Поиск начинается с папки , которая содержит представления, выделенные для те­ кущего контроллера. В имени этой папки опускается часть Contro ller имени клас­ са, так что папкой для класса HomeController будет Views/Home. Если имя представления в объекте ViewResul t не указано, тогда будет исполь­ зоваться значение переменной action из данных маршрутизации . Для большинс­ тва контроллеров это означает применение имени метода действия, следовательно, стандартным файлом представления, ассоциированным с методом Index () , является Index . cshtml . Однако если вы использовали атрибут Route, то имя представления, связанн ое с методом действия, может быть другим. Если контроллер принадлежит области, как было описано в главе 16, тогда место­ положения поиска отличаются: /Аrеаs/<ИмяОбласти>/Viеws/<ИмяКонтроллера>/<ИмяПредставления> . сshtтl /Аrеаs/<ИмяОбласти>/Viеw s/Sh аrе d/<ИмяПр едставления> . сsh tтl /V iews/Shared/<ИмяПpeдcтaвлeния>.cshtml Инфраструктура MVC проверяет, существует ли каждый из перечисленных фай­ лов. по очереди. Как только обнаруживается совпадение, она применяет найденное представлен ие для визуализации результата метода действия. Области в примере
522 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC проекта не используются, поэтому метод действия из листинга 17 . 18 заставит MVC начать поиск с файла Views/Home/Result . cshtml . Такого файла н е существует, так что MVC продолжит поиск для файла Views/Shared/Resul t . cshtml . который буд ет найден и применен для визуализации НТМL-ответа. Модульное тестирование: визуализация представления Чтобы протестировать представление, которое визуализирует метод действия, можно про­ инспектировать возвращаемый им объект ViewResu l t . Это не совсем то же самое (в кон ­ це концов, вы не следуете процессу вплоть до проверки финальной НТМL-разметки, которая была сгенерирована), но достаточно близко к реальности, т.к . позволяет удостовериться в корректной работе системы представлений MVC . В проект модульного тестирования добав­ лен новый файл по имени ActionTests . cs для хранения модульных тестов, задейство­ ванных в настоящей главе . Первой тестируемой ситуацией является момент, когда метод действия выбирает специфи­ ческое представление: puЬlic ViewResult ReceiveForm(string name , string city) => View( " Result ", $ " {name} lives in {city} " ) ; Можно определить, какое представление было выбрано, прочитав свойство ViewName объ­ екта ViewResul t, как показано в следующем тестовом методе: using ControllersAndActions . Controllers ; using Microsoft . AspNetCore . Mvc ; using Xunit ; namespace ControllersAndActions . Tests puЬlic class ActionTests ( [Fact] puЫic void Vi ewSelected() 11 Организация HomeController controller = new HomeController(); 11 Действие ViewResult result = controller . ReceiveForm( " Adam ", " London " ) ; 11 Утверждение Assert . Equa l ( " Result ", result .View Name); При тестировании метода действия , который выбирает стандартное представление, возни­ кает небольшая вариация: puЫic ViewResult Result() => View(); В таких ситуациях необходимо удостовериться, что именем представления является nul l: Assert .N ull(result .V iewName) ; С помощью значения null объект ViewResul t сигнализирует MVC о том, что было вы­ брано стандартное представление, ассоциированное с методом действия .
Глава 17. Контроллеры и действия 523 Указание представления по его пути Подход с соглашением об именовании для представлений удобен и прост, но он ограни­ чивает представления, которые можно визуализировать. Если требуется визуализировать специфичное представление, то для этого можно предоставить явный путь и пропустить стадию поиска. Ниже приведен пример: using Microsof t. AspNetCo r e . Mv c; namespace Control l e rsAndAct ion s . Co nt ro l lers { puЫic cl a s s ExampleContr oller : Controller { puЬlic ViewResult Index() { return View("/Views/Admin/Index"); При таком указании представления путь должен начинаться с / или ~ / и может включать расширение имени файла (которым будет . c shtml, если оно не указано). Когда вы обнаруживаете , что пользуетесь этим средством, возьмите паузу и задайте себе вопрос : чего вы стараетесь достичь? Если вы пытаетесь визуализировать представление , при ­ надлежащее другому контроллеру, тогда может быть лучше перенаправить пользователя на метод действия в другом контроллере (пример ищите в разделе "Перенаправление на метод действия" далее в главе). Если вы пытаетесь обойти схему именования файлов представле­ ний, поскольку она не соответствует способу организации вашего проекта, тогда обратитесь в главу 21, в которой объясняется реализация специальной последовательности поиска. Передача данных из метода действия в представление Когда объект Vi e wRes ul t используется для выбора представления, из метода дейс­ твия можно передавать данные для применения при генерации НТМL-содержимого . Инфраструктура MVC предлагает различные способы передачи данных из метода действия в представление, которые описаны в последующих разделах. Эти средства естественным образом касаются темы представлений, которая более подробно рас- 1tрывается в главе 21. В настоящей главе мы обсудим только ту функциональность представлений , которой достаточно для демонстрации средств контроллеров. Использование объекта модели представления Отправить объе1tт представлению можно путем его передачи в качестве парамет­ ра методу View () , что приведет к установке свойства Vi ewData . Mo d e l созданного объекта ViewResul t . В листинге 17.9 указанное свойство устанавливалось напря­ мую , чтобы объяснить, как работают контроллеры РОСО, но метод View () позабо­ тится об этом более лаконичным образом. В листинге 17.19 п01~а з ан новый класс Examp l e Controll er. добавленный в папку Con t rolle rs, который передает объект модели представления методу View ( ). Листинг 17.19. Содержимое файла ExampleController.cs из папки Controllers using Micr osof t. AspNetC o re.Mvc; using Sys tem; namespace ControllersAn dAc tio n s . Contro l l e rs puЫic cl ass ExampleContr oller : Contr oller { puЫic ViewResul t Index() => View(Dat eTime.Now) ;
524 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Объект DateTime передается методу View () для применения в качестве модели представления . Для доступа к объекту изнутри представления используется ключевое слово Model механизма Razor. Далее создается папка Views/Example, куда помеща­ ется файл представления по имени Index. cshtml, содержимое которого приведено в листинге 17.20. Листинг 17.20. Содержимое файла Index. csh tml из папки Views/Example @{ Layout = null ; < ! DOCTYPE html> <html> <head> <meta name="viewport" content= " width=device - wi dth " /> <title>Controllers and Actions</title> <link rel= "stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> </head> <body class= " panel-body " > Model : @(( (DateTime)Mode l) .DayOfWeek) </body> </html > Представление в листинге 17.20 называется нетuпuзированным или слабо типи­ зированным. Такому представлению ничего не известно об объекте модели представ­ ления, и оно трактует его как экземпляр obj ect. Чтобы получить значение свойства DayOfWeek, экземпляр object понадобится привести к типу DateTime: Model : @(( (DateTime) Model) .DayOfW eek ) Прием работает, но порождает запутанные представления. Навести порядок мож­ но за счет создания строго типизированных представлений, когда представление включает детали типа объекта модели представления, как демонстрируется в лис­ тинге 17.21 . Листинг 17.21 . Добавление строго типизированного представления в файл Index. csh tml из папки Views/Exarnple @model DateTime @{ Layout = null ; < ! DOC TYPE html> <html> <head> <meta name="viewport " content= " width=device - width" /> <title>Controllers and Actions</title> <link rel= " stylesheet " asp -h ref - inc lude="li Ь/bootstrap/dist/css/* . min . css " /> </head> <body class= " panel-body " > Model: @Model.DayOfWeek </body> </html>
Глава 17. Контроллеры и действия 525 тип модели представления указывается с применением ключевого слова mo del синтаксиса Razor. Обратите внимание на использование буквы m нижнего регистра при указании типа модели и буквы М верхнего регистре при чтении значения. Строгая типизация не только помогает привести в порядок представление , но так­ же позволяет Visual Studio поддерживать средство IntelliSense (рис. 17.7). · lJ< t1eao ... j <meta narre• " vie\'lpoгt " content •",,..idt h•devi ce- \'li dth" /> 1 <title >Controllers • nd Act i ons </title> ; <link reJ •"stylosheet" osp·href-include•"liЬ/Ьcotstrap/dist/cso/ • . css" /> </l1ead> . -.- .; <body class•"p-ll · l"> I ~>odol : ёf'lodel .j ; </Ьоdу > ф AddYeщ . . </html> Ф ComparoTo ,1- Dщ /' Day -1' •. /' DayOfYear (i' Equal s Ф GetDateTimeFormats ~ GetHa<hCode D•yOIWocl: Dot•Тinle. D•yOfWeek ( get;} Gets the d•y of the >vtek 1'prt<ented Ьу t hi~ instanco. Рис. 17. 7. Поддержка средства lntelliSense для строго типизированных представлений Модульное тестирование: объекты модели представления Объекты модели представления присваиваются свойству ViewResul t . ViewDa ta . Model, а это означает возможность проверки того, что метод действия отправляет ожидаемые дан­ ные, когда вызывается метод View (). Вот тестовый метод, который проверяет тип модели для метода действия из листинга 17 .20 : [Fact] puЫic void ModelOb jectType() { 11 Организация Examp l eController controller = new ExampleController(); 11 Действие ViewResu lt result = controller .I ndex() ; 11 Утверждение Assert .IsType<System.DateTime >(res ult.ViewData . Model) ; Метод Assert . IsType () применяется для проверки того, что объект модели представ­ ления является экземпляром DateTime . С использованием метода View () связано одно затруднение , которое возникает, когда нужно задействовать стандартное представление, ассоциированное с действи­ ем, и снабдить его объектом модели string (листинг 17 .22).
526 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 17.22. Использование метода View () в файле ExampleController. cs usi ng Mi crosoft.AspNetCore.Mvc; usi ng System; n amespace Co n trollersAndActions . Controlle r s puЫic class Examp l eController : Co n troller { p u Ьlic ViewResu l t Index() => View(DateT ime.Now) ; puЫic ViewResult Result() => View("Hello World"); В новом методе действия Resul t () необходимо применить метод View (), кото­ рый ви зуал изирует стандартно е представление для действия , и указ ать д анны е мо­ дели, что соответствует треть ей версии этого метода из табл. 17.8. Но посл е запус­ ка приложения и запроса URL вида /Example /Resu l t будет получ ено сообще ни е об ошибке вроде показанного ниже : Inv alidOpe rationExceptio n: The view 'H e l lo , Wor l d ' was not found. The f ol lowing locat ions were searched : /Vi ews/Example/Hell o , Wo rld . cshtml /Vi ews/Shared/He l lo , Worl d. cshtml InvalidOpera t ionException : Предст а вление ' Hello , World ' не найдено . П оиск производился в следующих мес т оположениях : /Views/Example/Hello , World . cshtml /Views/Shared/Hello , World . cshtml Проблема в том, что вызов метода Vi ew ( ) с объектом string дал совпадение со второй версией данного метода из табл. 17.8 , т. е . аргумент s tr ing был инт е рпрети­ рован как имя пр едставления , подлежащего визуализации, поэтому инфраструктура МVС пыталась найти файл представления под названием Hel l o , World . cshtml , а н е Re sul t . cshtml. Это распространенная проблема , но ее легко исправит ь , выполнив приведение данных модели к типу obj ect (листинг 17.23). Листинг 17.23. Выбор корректного метода View () в файле ExampleController . cs using Microsoft . AspNe t Core . Mvc ; using System ; namespace Contro l lersAndActions . Contro l lers puЫic class Exampl eControl ler : Controll er { puЬlic ViewResult Index() => View(DateT i me . Now) ; puЫic ViewResult Result() => View( (object) "Hello World"); Явное приведение данных модели к object гарантирует соотв етствие вы з ова пра­ вильной версии метода Vi ew () и обеспечит визуализацию представления из файла Resul t . cshtml.
Глава 17 . Контроллеры и действия 527 Передача данных с помощью ViewBag Объ ект ViewBag был описан в главе 2. Это средство позволяет определять свойс­ тва в динамическом объекте и получать доступ к ним в представлении. Динамический объект доступен через свойство ViewBag, предоставляемое классом Controller, как демонстрируется в листинге 17.24 . Листинг 17 .24. Использование объекта ViewBag в файле ExarnpleController. cs using Microsoft.AspNetCore.Mvc; using System ; namespace ControllersAndActions . Controllers puЫic class ExampleController : Contro l ler { puЫic ViewResult Index( ) { ViewBag.Message = "Hello"; ViewBag.Date = DateTirne.Now; return View () ; puЫic ViewResult Result() => View( (object) " Hello World " ) ; Свойства Message и Date объекта ViewBag определяются путем присваивания им з начений . До этого момента указанные свойства не существовали, и никакая подгото­ вительная работа для их создания не выполнялась. Чтобы прочитать данные в пред­ ставлении , извлекаются значения из тех же самых свойств , которые устанавливались в методе действия (листинг 17 .25) . Листинг 17.25 . Чтение данных из объекта ViewBag в файле Index. cshtrnl из папки Views/Exarnple @model DateTime @{ Layout = null ; <IDQCTYPE html> <html> <head> <meta name= " viewport " content= " width=device-width " /> <title>Controllers and Actions</title> <link rel="stylesheet" asp - h ref-in clude= "li Ь/bootstrap/dist/css/* . min . css " /> </head> <body class= "p anel - body " > <p>The day is: @ViewBag. Date. DayOfWeek</p> <p>The message is: @ViewBag . Message</p> </body> </html> Преимущество применения объекта ViewBag перед объектом модели представле­ ния связано с легкостью отправки множества объектов представлению. Если бы инф­ раструктура МVС поддерживала только модели представл ений, то для получения того же результата пришлось бы создавать новый тип с членами string и DateTime .
528 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Внимание! Среда Visual Studio не способна предоставить поддержку lntelliSense для любых динамических объектов, включая ViewBag , и ошибки не будут обнаружены до тех пор , пока не произойдет визуализация представления . Модульное тестирование: объект ViewBag Свойство ViewResu l t . ViewDa ta возвращает словарь, ключами которого являются име­ на свойств ViewBag, определяемых методом действия . Вот тестовый метод для метода действия из листинга 17.24: [Fact] puЫic void Mode l ObjectType() 11 Организация ExampleController controller = new ExampleController(); 11 Действие ViewResult result = controller . Index{) ; 11 Утверждение Assert.IsТype<string>(result.ViewData["Message"]); Assert.Equal("Hello", result.ViewData["Message"]); Assert.IsType<System.DateTime>(result.ViewData["Date"]); Этот тестовый метод проверяет типы свойств Message и Date , используя метод Assert. IsType () , и проверяет значение свойства Message с применением метода Assert . Equal (). Выполнение перенаправления Обычный результат из метода действия предназначен не для выпуска какого-ли­ бо вывода напрямую, а для перенаправления клиента на другой URL. Большую часть времени таким URL является другой метод действия внутри приложения, который генерирует вывод, подлежащий отображению пользователям. Когда выполня ется пе­ ренаправление , браузеру отправляется один из следующих двух кодов НТГР. • Код НТГР 302, который означает временное перенаправл ение. Это наиболее час­ то используемый тип перенаправления и в случае применения паттерна Post/ Redirect/Get необходимо посылать данный код . • Код НТГР 301, который означает постоянное перенаправление. Этот код должен использоваться с осторожностью, т.к. он инструктирует получат еля не запраши­ вать снова исходный URL, а применять новый URL , вмюченный вместе с кодом перенаправления. В случае сомнений используйте временное перенаправление, т.е. отправляйте код 302. Для выполнения перенаправления могут применяться и другие р ез ультаты дейс­ твий, которые описаны в табл. 17.9 .
Глава 17. Контроллеры и действия 52 9 Табл и ца 17.9 . Результаты действий для перенаправления Имя Red i rectResult LocalRedirectResult М етод класса Controller Redirect Redi rectPermanent LocalRedirect Local Redi r ectPermanent RedirectToActionResul t Redi rectToAction Описание Этот результат действия посылает ответ с кодом со­ стояния НТТР 301 или 302, выполняя перенаправление клиента на новый URL Этот результат действия вы­ полняет перенаправление клиента на локальный URL Этот результат действия RedirectionToAc tionPerrnanent выполняет перенаправле­ ние клиента на указанные действие и контроллер RedirectToRouteResult RedirectToRoute RedirectToRou tePermanent Пер енаправление на буквальный URL Этот результат действия выполняет перенаправ­ ление клиента на URL, сгенерированный из спе­ цифичес кого маршрута Наибол ее базовый способ перенаправления браузе ра пр едусматривает вызов мето­ да Redirect () ,пр едоставляе м ого классом Controller , которы й возвращает экзем ­ пляр класса RedirectResul t (листинг 17 .26). Листи нг 17.26. Перенаправлен ие на букв альный URL в файле ExampleController. cs using Microsoft.AspNetCore . Mvc ; usi ng System; namespace ControllersAndActions . Control l ers puЬlic class Examp l eController : Controller { puЫic ViewResult Index() { ViewBag .Message = " He llo "; ViewBag.Date = DateT i me . Now ; return View() ; puЬlic ViewResult Result() => View( (object) " Hello World " ); puЬlic Re directResul t Redirect () => Redirect ( "/Example/Index") ; Здесь URL перенаправления выражается как аргумент string метода Red i rect (), который обеспечивает временное перенаправление. Постоянное перенаправление м ож но выполнить с помощью м етода RedirectPermanent (), как по каза но в лис­ тинге 17.27.
530 Часть 11. Подробные сведения об инфраструктуре ASP. NET Core MVC Совет. Объект LocalRedirectionResul t - это альтернативный результат действия, который будет генерировать исключение, если контроллер попытается выполнить пере­ направление на любой URL, не являющийся локальным . Он полезен в случае, если осу­ ществляется перенаправление на URL , предоставляемые пользователями, когда пред­ принимается попытка атаки открытым перенаправлением для перенаправления другого пользователя на ненадежный сайт. Такой вид результата действия может быть создан пос­ редством метода LocalRedirect (), унаследованного от класса Controller. Листинг 17.27. Постоянное перенаправление на буквальный URL в файле ExampleController. cs using Microsoft . AspNetCore . Mvc; using System ; namespace ControllersAndActions . Controllers puЫic class ExampleController : Controller { puЫic ViewResult Index() { ViewBag.Message = "Hello"; ViewBag.Date = DateTime . Now ; return View () ; puЫic ViewResult Result () => View ( (object) " Hello World " ); puЬlic RedirectResult Redirect{) => RedirectPermanent("/Example/Index"); Модульное тестирование: перенаправление на буквальный URL Перенаправление на буквальный URL тестировать легко. С помощью свойств u r 1 и Permanent класса RedirectResult можно прочитать URL и вид перенаправления (пос­ тоянное или временное). Вот тестовый метод для постоянного перенаправления из листин­ га 17.27: [Fact] puЫic void Redirect i on () { 11 Организация ExampleController controller = new Examp l eController(); 11 Действие RedirectResult result = controller . Redirect() ; 11 Утверждение Assert .Equal( " /Example/Index ", result.Url) ; Assert . True(result . Permanent); Обратите внимание , что тест обновлен для получения объекта RedirectResul t при вы­ зове метода действия .
Глава 17. Контроллеры и действия 531 Перенаправление на URL системы маршрутизации При перенаправлении пользователя на другую часть прилож е ния необходимо удостовериться в том, что отправляемый URL является допустимым в рамках схемы URL. Проблема, связанная с использованием для перенаправления буквальных URL, в том, что любое изменение в схеме маршрутизации означает необходимость пересмот­ ра кода и обновления таких URL. К счастью, можно задействовать систему маршрути­ зации для генерирования допустимых URL с помощью метода RedirectToRoute (), который создает экземпляр класса RedirectToRouteResult (листинг 17.28). Совет. Если вы последовательно прорабатываете примеры из настоящей главы, тогда для нормального функционирования кода в листинге 17.28 может понадобиться очистить хро­ нологию браузера. Причина в том, что браузер запомнит постоянное перенаправление в листинге 17.27, и будет транслировать запрос URL вида /Example/Redirect в запрос /Example / Index, не контактируя с сервером. Листинг 17.28. Перенаправление на URL системы маршрутизации в файле ExampleController. cs using Microsoft.AspNetCore . Mvc ; using System ; namespace ControllersAndActions.Controllers puЬlic class ExampleController : Controller { puЫic ViewResult Index() { ViewBag . Message = "H el lo"; ViewBag.Date = DateTime .N ow ; return View () ; puЫic ViewResult Result() => View( (object) "Hello World"); puЫic RedirectToRouteResult Redirect() => RedirectToRoute (new { controller = "Example", action = "Index", ID= "MyID"}); Метод RedirectToRoute () выпускает временное перенаправлени е . Для постоян­ ного перенаправления применяйте метод RedirectToRoutePermanent (). Оба мето­ да принимают анонимный тип, свойства которого затем передаются системе марш­ рутизации для генерирования URL , как объяснялось в главе 16. Модульное тестирование: перенаправление на URL системы маршрутизации Ниже приведен модульный тест для метода действия из листинга 17.28: [E'act] puЫic void Redirection() { 11 Организация ExampleController controller new ExampleController() ;
532 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC 11 Действие RedirectToRouteResult result = controller.Redirect(); 11 Утверждение Assert.False(result.Permanent); Assert.Equal("Example", result.RouteValues["controller"]); Assert.Equal("Index", result.RouteValues["action"]); Assert.Equal("MyID", result.RouteValues["ID"]); Результат проверяется косвенным образом за счет просмотра информации о маршруте, предоставляемой объе ктом RedirectToRouteResul t , поэтому нет необходимости в разборе URL , что потребовало бы допущений в модульном тесте о схеме URL, используе­ мой приложе нием. Перенаправление на метод действия Выполнить перенаправление на метод действия можно более элегантно с приме­ нением метода RedirectToAction () (для временного перенаправления) или мето­ да RedirectToAct i onPermanent () (для постоянного перенаправления). Это просто оболочки вокруг метода RedirectToRoute (), которые позволяют указывать значе­ ния для метода действия и контролл ера без необходимости в создании анонимного типа (листинг 17.29). Листинг 17.29. Перенаправление с использованием метода RedirectToAction () в файле ExampleController. cs using Microsoft.AspNetCore.Mvc ; using System ; namespace ControllersAndActions . Control le rs puЫic class ExampleController : Controller { puЫic ViewResult Index() { ViewBag.Message = " Hel l o "; ViewBag . Date = DateTime.Now; r eturn View () ; puЬlic RedirectToActionResult Redirect () => RedirectToAction (" Index") ; ) Если указан только метод действия , то предполагается , что он относится к те1<у­ щему контроллеру. Для перенаправления на другой контроллер понадобится предо ­ ставить имя контроллера как параметр: p uЬlic RedirectToActionResult Redirect() => RedirectToAction("Index", "Home"); Доступны и другие перегруженные версии, с помощью которых можно предостав­ лять дополнительные значения для генерации URL. Они выражаются с примен е нием анонимного типа , который отнюдь не умаляет пользу от удобного метода, но может облегчить восприятие кода.
Глава 17. Контроллеры и действия 533 На заметку! Значения, предоставляемые методу действия и контроллеру, не проверяются перед их передачей системе маршрутизации. Вы сами отвечаете за то, что указываемые цели действительно существуют. Модульное тестирование: перенаправления на метод действия Вот модульный тест для метода действия из листинга 17 .29: [ Fact] puЫic void Re direction() { //Ор г анизация Examp l eControl ler control l er = ne w Ex ampl e Controller() ; 11 Действ и е Red i rectToActio nR esult resul t = c o ntro lle r .Redi r ect() ; 11 Утверждение Asser t .False (re sult .Permane nt ) ; Asser t. Equal ("Index ", result.ActionName); В классе Redirec tT o Act i o n Res ul t определены свойства Cont r o l l erName и Ac t ionNarne , которые упрощают инспектирование перенаправления, созданного контрол­ лером, и не требуют разбора URL. Использование паттерна Post/Redirect/Get Перенаправление чаще всего применяется в методах действий, которые обраба­ тывают НТТР-запросы POS T. Как объяснялось в предыдущей главе. запросы POST используются, когда нужно изменить состояние приложения. Если после обработки запроса POST просто возвращать НТМL-ответ, то есть риск того, что пользователь щелкнет на кнопке перезагрузки страницы в браузере и отправит форму во второй раз, приводя к непредсказуемым и нежелательным результатам . Наблюдать такую проблему можно в контроллере Home внутри примера приложе­ ния. Метод Rece i veForrn ( ) принимает параметры, значения которых получаются из данных формы. и применяет метод Vi ew () для возвращения объекта ViewResul t: puЬlic Vi ewRes ult ReceiveForm(st ring name , s tring city) => View("Result", $"{narne} lives in {city}"); Чтобы взглянуть на проблему, запустите приложение и запросите URL вида /H ome . Отправьте форму и затем щелкните на кнопке перезагрузки страницы в браузере. С помощью инструментов, доступных по нажатию клавиши <F12>. изучите НТТР­ запросы. сделанные браузером; вы заметите, что серверу был отправлен новый за­ прос PO ST. В таком простом приложении он не окажет какого-либо влияния, но эта проблема может вызвать разрушения, если запросы PO ST в итоге удаляют данные, отправляют заказы или выполняют другие важные задачи, которые пользователь не намеревался предпринимать.
534 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Чтобы избежать возникновения описанной проблемы, можно следовать паттерну под названием Post/Redirect/Get. В соответствие с паттерном Post/Redlrect/Get вы получаете запрос PO ST , обрабатываете его и затем выполняете перенаправление бра­ узера, так что он делает запрос GE T для другого URL. Запросы GET не долж ны моди­ фицировать состояние приложения, поэтому случайные повторные отправки данного запроса н е вызовут никаких проблем. В листинге 17.30 добавлено п е ренаправ ление браузера на другой URL с помощью запроса GET. Листинг 17.30. Реализация паттерна Post/Redirect/Get в файле HomeController.cs using Microsoft.AspNetCore.Mvc; using Systern .Text ; u sin g Co ntro l lersAndA ctions .Infrast ru ct ure; narnespace Cont r oll ersAndActions .Controllers puЫic cl ass HorneController : Cont r oll er { puЫic ViewResult Index() => View("SirnpleForrn"); [HttpPost] puЫic RedirectToActionResult ReceiveForm(string name, string city) => RedirectToAction(nameof(Data)); puЫic ViewResult Data() => View("Result"); Метод Redirect To Acti o n Resul t получает данные от пользователя через запрос POST и выполняет перенаправление клиента на метод действия Data (). Если пользо­ ватель перезагрузит страницу, тогда методу действия Data () посыла ется безвредный запрос GE T. Атрибут Http Po s t , который будет описан в главе 20, гарантирует воз­ можность отправки действию Rece ive F o r () только запросы POS T. Использование объекта TempData Перенаправление заставляет браузер послать полностью новый НТТР-запрос , что означает отсутствие доступа к данным формы из исходного запроса. Таким образом, методу Da t a ( ) ничего не известно о значениях n a me и ci ty, которы е должны быть отображены пользователю. Если необходимо предохранить данные между запросами, тогда можно применить объект Te mpD ata . Объект TempData похож на данные сеанса, которые использовались в главе 9 , за исключением того, что значения TempDa ta помечаются для удаления, когда они прочитаны , и удаляются из хранилища данных после обработки запроса. Это идеальный вариант для краткосрочных данных, которые необходимы для того, чтобы перенаправление работало в паттерне Post/Redirect/Get. Объект TempData до­ ступен через свойство TernpD at a класса Co nt rol ler (листинг 17.31). На заметку! Объект TempData полагается на промежуточное ПО сеанса . В начале главы приводился список требуемых пакетов NuGet в файле p r oj ect. j son и операторы кон­ фигурирования для класса S t artup .
Глава 17 . Контроллеры и действия 535 Листинг 17.31. Использование объекта TempData в файле HomeControl l er . cs using Microsoft . AspNetCore . Mvc ; using System . Text ; u s ing ControllersAndActions.I nfra s t ru cture; namespace Con t rollersAndAct i o ns. Co n tro ll e r s puЫic class HomeControl l er : Con tr ol ler puЫic ViewResu l t I n dex() ( ret urn View( "SimpleForm"); [HttpPost ] p uЫ ic RedirectToActi onRe s u lt ReceiveForm(st r i ng name , s tring c ity) { TempData [ "name"] = name; TempData["city"] = city; r eturn Redi rectToActi on(nameof (Data)) ; puЫic ViewResul t Da t a() { string name = TempData [ "name"] as string; string city = TempData["city"] as string; return View("Result", $" {name} lives in {city} "); Метод Rece i ve F orm () применяет свойство Temp Da t a . возвращающее словарь. для сохранения значений name и c i ty перед перенаправлением кли ента на действие Data. Метод Data () использует то же самое свойство Te mpData для извлеч е ния зна­ чений данных и их применения при создании данных модели, которые будут отобра­ жаться представлением. Совет. Словарь TempDa t a также предоставляет метод Pee k: () , который позволяет полу­ чить значение данных, не помечая его для удаления, и метод Ке ер ( ) , который можно ис­ пользовать, чтобы предотвратить удаление ранее прочитанного значения . Метод Кеер ( ) не защищает значение навсегда. Если значение прочитано снова, оно будет помечено для удаления еще раз . Если вы хотите хранить элементы, чтобы они не удалялись, когда за­ прос будет обработан, тогда применяйте данные сеанса. В оз вр ащение разных типов содержимого HTML - не единственны й вид отв ета , который способны генерировать методы действий, и в табл. 17.10 описаны встроенные результаты действий, которые можно использовать для разных типов данных. Генерирование ответа JSON Формат JSON (JavaScript Object Notation - система обозн а чений для объектов JavaScript) стал стандартным способом передачи данных между веб-приложением и ero клиентом. Форм ат JSON в значительной степени заместил ХМL в кач естве форма­ та обмена , поскольку с ним проще работать, особенно при написании кода JavaScript клиентской стороны, т.к. JSON тесно связан с синтаксисом, который JavaScript ис­ пользует для опр еделения лит е ральных значений данных.
536 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Таблица 17.1 О. Результаты действий для содержимого Имя J sonRes ult Conten t Resul t ObjectResult OkOb j ectRe sult NotFoundOb jectResult Метод класса Controller J son Content Ok NotFound Описание Этот результат действия сериализирует объект в формат JSON и возвращает его клиенту Этот результат действия отправляет ответ, тело которого содержит указанный объект Этот результат действия будет применять согласо­ вание содержимого для отправки объекта клиенту Этот результат действия будет использовать со­ гласование содержимого для отправки объекта клиенту с кодом состояния ~ПР 200, если со­ гласование содержимого успешно Этот результат действия будет применять со­ гласование содержимого для отправки объекта клиенту с кодом состояния НТТР 404, если со­ гласование содержимого успешно Тема формата JSON и его роли в веб -приложенилх раскрывается в главе 20, а в листинге 17.32 демонстрируется применени е метода J son ( ) для создания объекта JsonResult . Листинг 17.32. Генерирование ответа JSON в файле ExampleCon troller. cs using Micros oft . AspNe t Core . Mvc ; us i ng Systern; narnespace Co ntrol lersAndActions . Co nt r ollers puЫic class ExarnpleCont roller : Cont roller { puЬlic JsonResult Index() => Json(new[] { "Alice", "ВоЬ", "Joe" }) ; Запустив приложение и запросив URL вида / Exam ple , вы получите ответ. который выражает строковый мас сив С# из метода действия в формате JSON: ["Alice'', "ВоЬ", "Joe"] Некоторые браузеры будут отображать результаты JSON встроенным образом . тог­ да как другие, включая Microsoft Explorer, требуют сохранения данных в файл, прежде чем их можно будет инспектировать. Модульное тестирование: результаты действий, отличающиеся от HTML Важно помнить, что модульные тесты для метода действия должны быть сосредоточены на данных, возвращаемых с целью последующего форматирования, а не на самом формати­ ровании, которое поддерживается инфраструктурой MVC и обычно выходит за рамки боль­ шинства проектов тестирования . В качестве примера ниже показан модульный тест для ме­ тода действия из листинга 17.32:
[Fact] puЫic void JsonActionMethod() 11 Организация ExampleController controller 11 Действие Глава 17. Контроллеры и действия 537 new ExampleController(); JsonResult result = controller.GetJson(); 11 Утверждение Assert.Equal(new[J { "Alice", "ВоЬ", "Joe" }, result.Value); Класс JsonResult предлагает свойство Value, возвращающее данные, которые будут преобразованы в формат JSON для формирования ответа клиенту. В модульном тесте про­ изводится сравнение свойства Value с данными, которые ожидается получить. Использование объектов для генерации ответов Многие приложения нуждаются только в ответах HTML и JSON от контроллеров, а при доставке других типов содержимого, таких как изображения, файлы JavaScript и таблицы стилей CSS, рассчитывают на поддержку для статических файлов. Тем не менее, могут возникать ситуации, когда необходимо возвращать в ответе содержимое специфического типа, и для помощи в этом доступны соответствующие результаты действий. Простейшим является класс ContentResul t, создаваемый посредством метода Content (),который применяется для отправки значения string с дополни­ тельным типом содержимого MIME. В листинге 17.33 метод Content () используется для воссоздания вручную результата JSON из предыдущего раздела. Листинг 17.33. Ручное создание результата JSON в файле ExampleController. cs using Microsoft.AspNetCore.Mvc; namespace ControllersAndActions.Controllers puЫic class ExampleController : Controller { puЫic ContentResult Index() => Content (" [\ "Al.ice\", \ "ВоЬ\", \ "Joe\"] ", " appl.icat.ion/json") ; Такой тип результата действия полезен, когда имеется содержимое, которое удобно представлять в формате string, и известно, что клиент способен принимать указы­ ваемый тип MIME. Опасность этого подхода в том, что клиенту может быть отправлен ответ в формате. который он не в состоянии обработать. Более надежный подход по­ лагается на согласование содержимого, которое выполняется классом Obj ectResul t (листинг 17.34). Листинг 17.34. Применение согласования содержимого в файле ExampleController. cs using Microsoft.AspNetCore.Mvc; namespace ControllersAndActions.Controllers
538 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC puЫic class ExampleController : Controller { puЫic ObjectResult Index() => Ok(new string[) { "Alice", "ВоЬ", "Joe" }) ; Термин согласование содержимого намекает на сложную систему выявления об­ щего формата между браузером и приложением, но в действительности представляет собой простой процесс. Когда браузер делает НТТР-запрос, он включает в него заго­ ловок Accept, который указывает, какие форматы он может обрабатывать. Вот как выглядит такой заголовок в версии Google Chrome, используемой для тестирования примера: Accept : text/html,application/xhtml+xml,application/xml ; q=0.9,image/webp,*/*;q=0 . 8 Поддерживаемые форматы выражаются как типы MIME. Инфраструктура MVC имеет набор форматов, которые она может применять для значений данных, и срав­ нивает их с форматами, поддерживаемыми браузером. Пр едпочтительным форма­ том, используем ым инфраструктурой MVC, является JSON, и он будет применяться в большинстве случаев кроме ситуации, когда действие возвращает значение string и используется простой текст. Процесс согласования содержимого и особенности его реализации более подробно рассматриваются в главе 20. Реагирование с помощью содержимого файлов Большинство приложений при доставке содержимого файлов рассчитывают на промежуточное ПО статических файлов, но есть также набор результатов действий. которые можно применять для отправки файлов клиенту {табл. 17.11). Внимание! Будьте осторожны при использовании этих результатов действий, чтобы не создать приложение, которое позволит запрашивать содержимое произвольных файлов . В частнос­ ти, не получайте путь к подлежащему отправке файлу из любой части запроса либо из лю­ бого хранилища данных , которое пользователь может модифицировать через запрос. Таблица 17 .11 . Результаты действий для файлов Имя FileContentResult FileStreamResult VirtualFileResult PhysicalFileResult Метод класса Controller File File File PhysicalFile Описание Этот результат действия посылает клиенту байтовый массив с указанным типом MIME Этот результат действия читает поток и отправляет содержимое клиенту Этот результат действия читает поток из виртуального пути (относительного к ката­ логу, где размещается прило жение ) Этот результат действия читает содержи­ мое файла из указанного пути и посылает его клиенту
Глава 17 . Контроллеры и действия 539 В листинге 17.35 метод File (), унаследованный от класса Controller, приме­ няется для возвращения СSS-файла Bootstrap в качестве результата метода действия Index () контроллера Examp le . Листинг 17.35. Использование содержимого файла в качестве ответа в файле ExampleCon troller. cs using Microsoft . AspNetCo r e . Mvc ; namespace ControllersAndActions.Controllers puЬlic class ExampleController : Controller { puЬlic VirtualFileResul t Index () => File("/lib/Ьootstrap/dist/css/bootstrap . css", "text/css"); Для применения этого метода действия понадобится модифицировать элемент link в файле Simp l eForm . cshtml, чтобы в нем использовался вспомогательный класс Url (листинг 17.36). Листинг 17.36. Нацеливание на метод действия в файле SimplerForm. cshtml @{ Layout = null; < !D OCTYPE html > <html> <head> <meta name =" viewport " content= " width=device - width " /> <title>Control lers and Actions</title> <link rel="stylesheet" href="@Url.Action("Index", "Example")" /> </head> <body class= " pane l- body " > <form method= " post " asp - action= " ReceiveForm " > <d iv class= "form - group " > <label for= " name " >Name : </l abel> <input c l ass= " form -control " name= " name " /> </div> <div class= " form-g r oup " > <label for= "name " >City : </lab el> <input class= " form -control " name= " city " /> </div> <button class= " btn Ьtn - pr i mary center - Ыock " type="submit">Submit </button> </form> </body> </html> После запуска приложения и запрашивания URL вида / Н оте отправленный брау­ зеру НТМL-ответ будет включать следующий элемент: <link rel= " stylesheet " href=" /Example " /> Он заставит браузер послать НТГР-запрос. нацеленный на метод действия из лис­ тинга 17 .35, который отправит файл CSS, требующийся для стилизации содержимого представления .
540 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC На заметку! Как будет показано в главе 25 , дескрипторные вспомогательные классы являют­ ся намного более удобным инструментом для доставки файлов CSS . Возвращение ошибок и кодов НТТР Финальный набор встроенных клас сов ActionResul t может применяться для от­ правки клиенту специфичных сообщений об ошибках и результирующих кодов НТГР (табл. 17 . 12). Большинство приложений не нуждается в таких ср едствах, поскольку ASP.NET Core и MVC генерируют эти виды результатов автоматически. Однако они могут быть полезны в ситуациях, когда необходимо получить более прямой контроль над ответами, отправля ем ыми клиенту . Таблица 17.12. Результаты действий для кодов состояния Имя StatusCodeResult OkResult CreatedResult Created.AtActionResult Created.AtRouteResult BadRequestResult UnauthorizedResult NotFoundResult UnsupportedMediaTypeResult Метод класса Controller StatusCode Ok Created Описание Этот результат действия отправ­ ляет клиенту указанный код со­ стояния НТТР Этот результат действия от­ правляет клиенту код состояния НТТР 200 Этот результат действия от­ правляет клиенту код состояния НТТР 201 Created.At Action Этот результат действия отправ­ ляет клиенту код состояния НТТР 201 вместе с URL в заголовке Location, которы й нацелен на Created.AtRoute BadRequest Unauthorized NotFound действие и контроллер Этот результат действия от­ правляет клиенту код состояния НТТР 201 вместе с URL в заго­ ловке Location, который сге­ нерирован из специфического маршрута Этот результат действия от­ правляет клиенту код состояния НТТР 400 Этот результат действия от­ правляет клиенту код состояния НТТР 401 Этот результат действия от­ правляет клиенту код состояния НТТР 404 Этот результат действия от­ правляет клиенту код состояния НТТР 415
Глава 17 . Контроллеры и действия 541 Отправка специфического результирующего кода НТТР Отправить браузеру специфичный код состояния НТГР можно с использованием метода StatusCode () ,который создает объект StatusCodeResul t (листинг 17.37). Листинг 17.37. Отправка специфического кода состояния в файле ExarnpleController.cs using Microsoft . AspNetCore.Mvc; using Microsoft .AspNetCore .H ttp ; namespace ControllersAndActions . Contro ll ers puЫic class · ExampleController : Controller { puЬlic StatusCodeResul t Index () => StatusCode(StatusCodes . Status404NotFound); Метод StatusCode () принимает значение int, в котором можно напрямую указывать код состояния. Класс StatusCodes из пространства имен Microsoft . AspNetCore . Http определяет поля для всех кодов состояния, поддерживаемых НТГР. В листинге 17.37 применяется поле Status404NotFound для возвращения кода 404, который означает, что запрошенный ресурс не существует. Отправка результата 404 с использованием удобного класса Другие результаты действий, описанные в табл. 17 . 12 , расширяют или основаны на классе StatusCodeResult, который предлагает более удобный способ отправки специфических кодов состояния. Того же самого результата , что и в листинге 17.37, можно достичь с применением более удобного класса NotFoundResul t, который яв­ ляется производным от класса StatusCodeResul t и может быть создан с использо ­ ванием удобного метода NotFound () класса Controller (листинг 17.38). Листинг 17.36 . Генерирование результата 404 в файле ExarnpleController. cs using Microsoft . AspNetCore . Mvc ; using Microsoft . AspNetCore .H ttp ; namespace ControllersAndActions.Control lers puЫic class ExampleController : Controller { puЬlic StatusCodeResult Index () => NotFound (); Модульное тестирование: коды состояния НТТР Класс StatusCodeResul t следует шаблону, который вы уже видели для других ти­ пов результатов, и делает свое состояние доступным через набор свойств. В этом случае сво йство StatusCode возвращает числовой код состояния НТТР, а свойство StatusDescription - связанную описательную строку. Приведенный ниже тестовый метод предназначен для проверки метода действия из листинга 17 .28 :
542 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC [Fact] p uЬlic vo i d NotFou ndActionM e thod () 11 Организация ExampleControll er control ler = new ExampleCont roll er () ; 11 Действие St atusCodeRes ult result = cont rolle r.Index () ; 11 Утвержден и е Asser t. Equal(404 , res ul t . StatusCode); Другие классы результатов действий Некоторые дополнительные классы результатов действий тесно связаны со средства­ ми MVC , расс м атриваемыми в других главах. В табл. 17 .13 эти Юiассы кратко описаны со ссылками на главы, где обсуждаются средства, к которым они имеют отношен ие. Таблица 17 .13. Другие классы результатов действий Имя Part ialViewResult ViewCompon e ntResul t EmptyResult ChallengeResult Резюме Метод класса Controller Pa rti alView Vi ewCompo n ent Описание Этот результат действия применяется для выбора частичного представления , как объясняется в главе 21 Этот результат действия используется для выбора компонента представления , как описано в главе 22 Этот результат действия ничего не делает и производит пустой ответ для клиента Этот результат действия применяется для обеспечения соблюдения политик безо­ пасности в запросах . Подробные сведения приведены в главе 30 Контролл еры являются одним из основных строительных блоков в паттерне проек­ тирования MVC и находятся в це нтральной части разработки приложений MVC . В на­ стоящей главе было показ ано. как создавать контроллеры РОСО с испол ьзованием обычных классов С# и получать преимущество от удобства, предлагаемого базовым классом Cont r ol l er. Вы узнали, какую роль результаты действий играют в кон трол­ лерах МVС, и выяснили , каким образом они облегчают модульное тестирование. Были продемонстрированы различные способы получения ввода и генерации вывода из ме­ тода действия, а также встроенные результаты действий, которые делают этот про­ цесс простым и гибким. В следующей главе будет описано одно из средств, которо е вызывает наибольшую путаницу у разработчиков, использующих ASP.NET, но очень важно для эффективной разработки приложений МVС - внедрение зави сим остей.
ГЛАВА 18 Внедрение зависимостей в настоящей главе рассматривается внедрение зависимостей (dependency injection - DI) - методика, которая помогает создавать гибкие прило жения и уп­ рощает модульное тестирование. Внедрение зависимостей может оказаться трудной в понимании темой, как в план е того, почему оно может быть полезным, так и в отноше­ нии того, каким образом оно выполняется. В связи с этим мы будем двигаться медлен ­ но , начав с традиционного способа построения компонентов приложения и постепенно объясняя, как работает внедрение зависимостей и почему оно важно. В табл. 18.1 при ­ ведена сводка, позволяющая поместить внедрение зависимостей в контекст. Таблица 18.1 . П омещение внедрения зависимостей в контекст Вопрос Что это такое? Ответ Внедрение зависимостей облегчает создание слабо связанных компо­ нентов , что обычно относится к компонентам, которые потребляют функ­ циональность, определяемую интерфейсами, и не обладают непосредс­ твенным знанием о том, какие классы реализации используются Чем оно полезно? Внедрение зависимостей упрощает изменение поведения приложения пу­ тем изменения компонентов, которые реализуют интерфейсы, определя­ ющие фун кциональные средства приложения . Оно та кже дает в результате компоненты, которые легче изолировать для модульного тести рования Как оно используется? Существуют ли какие-то скры­ тые ловушки или ограничения? Существуют ли альтернативы? Изменилось ли оно по сравнению с версией MVC 5? С пом ощью класса Startup указывается, какие классы реализации применяются для доставки функциональности, определяемой интер­ фейсами, которые использует приложение . Когда для обработ ки запро­ сов создаются новые объекты, такие как контроллеры , они автомати­ чески снаб жа ются требуемыми экземплярами классов реализации Главное ограничение заключается в том, что классы объявляют о своем использовании служб в виде аргументов конструктора. Это может при­ вести к появлению конструкторов, единственной ролью которых являет­ ся получение зависимостей и и х присваивание полям экземпляра Вы не обязаны применять внедрение зависимостей в собственном коде, но полезно знать, каким образом оно работает, поскольку внедре­ ние зависи м остей используется инфраструктурой MVC для предостав­ ления фун к циональных средств разработчикам Предшествующие версии ASP.NET MVC были спроектированы так, чтобы сделать возможным внедрение зависимостей, но для его запуска в работу требовалось выбрать и установить сторонний инструмент. В ASP.NET Core MVC полная реализация DI в ключена как часть ASP.NET и широ ко применяется внутренне инфраструктурой MVC , хотя ее м ож но заменить сторонним пакето м
544 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC В табл. 18.2 приведена сводка для этой главы. Таблица 18.2. Сводка по главе Задача Создание слабо связанных компонентов Объявление зависимости в компоненте, таком как контроллер Конфигурирование отображе­ ния службы Модульное тестирование ком­ понента с зависимостью Указание способа для созда­ ния объектов реализации Изменение класса реализа ­ ции во время выполнения Получение зависимостей для отдельных методов действий в контроллере Запрос вручную объекта реа­ лизации в контроллере Решение Изолируйте классы посредством интер­ фейсов и соедините их вместе с исполь­ зованием внешних отображений Определите аргумент конструктора с ти­ пом, который требует компонент Добавьте отображение в класс Startup Создайте имитированную реализацию интерфейса службы и передайте ее как аргумент конструктора при создании ком­ понента в модульном тесте Создайте отображение службы с приме­ нением метода жизненного цикла, кото­ рый подходит управляемой службе Используйте метод жизненного цикла, который принимает фабричную функцию Примените внедрение в действия Используйте свойство HttpContext.RequestServices Подготовка проекта для примера Листинг 18 .1-18.18 18 .19 18.20, 18 .22-18 .28 18 .21 18 .29 , 18.30, 18 .32 , 18.33 18 .31 18.34 18.35 Для целе й этой главы создайте новый проект типа Empty (Пустой) по име­ ни Dependencyinj ection с применением шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuG et в раздел dependencies файла proj ect . j son и настройте инструментарий Razor в разд еле tools, как показано в листинге 18.1 . Разделы, которые не нужны для данной главы, понадобится удалить. Листинг 18.1. Добавление пакетов в файле proj ect. j son " dependencies ": { " Microsoft.NETCore . App ": "version": "1.0 .0", " type ": " platform " }, " Microsoft . AspNetCore . Diagnostics" : "1.0 .0", " Microsoft . AspNetCore . Server . IIS integration ": "1. 0 . 0 ", " Microsoft . AspNetCore. Server . Kestrel" : "1. О . О ", " Microsoft . Extensions .Logging . Console ": " 1 . 0 . О ",
Глава 18 . Внедрение зависимостей 545 "Microsoft. AspNetCore. Mvc" : "1. О. О" , " Мicr osoft. AspNetCore. StaticFiles" : "1. О. О", "Microsoft.AspNetCore.Razor .Tools": "version": 11 1. О. 0-preview2-final", "type": "Ьuild" }' "tools": "Microsoft . AspNetCore . Server .IISintegration . Tools ": " 1 . 0 . 0 - previ ew2 - f i nal ", "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final" )' " frameworks ": { " netcoreappl . 0 ": " irnports ": [ "dotnet5 . 6 ", " portaЫe-net4 5+w in8 " ] }' " buildOptions ": { " ernitEntryPoint ": true , " preserveCompilationContext ": true } , " runtimeOptions ": { " conf i gProperties ": { " System . GC .S erver ": true} В листинге 18.2 приведен класс Startup, который конфигурирует средства, пр е­ доставляемые пакетами NuGet. Листинг 18.2 . Содержимое файла Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions.Dependencyinjection ; narnespace Dependency in jection puЫic class Startup { puЫic vo id ConfigureServices(IServiceCollection services) { services.AddМvc(); puЫic vo id Confi gure( I App licationBui lder арр) { app.UseStatusCodePages(); app. UseDeveloperExceptionPage(); app .UseStaticFiles(); app.UseМvcWithDefaultRoute(); Создание модели и хранилища Для примеров этой главы необходима простая модель. Создайте папку Models и добавьте в нее файл класса по имени Product . cs с определением, показанным в лис­ тинге 18.3 .
546 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 18.3. Содержимое файла Product. cs из папки Models namespace Dependencyinjection.Models puЬlic class Product { puЬlic string Name [ get ; set ; } puЫic decimal Price { get ; set; Для управления моделью добавьте в папку Models файл класса по имени IReposi tory . cs и опр еделите в нем интерфейс, представленный в листинге 18.4 . Листинг 18.4. Содержимое файла IReposi tory. cs из папки Models using System . Collections . Generic ; namespace Dependencyinjection.Models puЫic interface IRepository { IEnumeraЫe<Product> Produc t s { get; Product this [string name] [ get ; } void AddProduct(Product product) ; void DeleteProduct(Product product); В интерфейсе IReposi tory определены операции , которые могут выполняться над коллекцией объектов Product. Чтобы предоставить реализацию этого интерфей­ са, добавьте в папку Models файл класса по им ени MemoryReposi tory . cs и по мес ­ тите в него содержимое из листинга 18.5 . Листинг 18. 5 . Содержимое файла MemoryReposi tory. cs из папки Model s using System . Collections . Generic ; namespace Dependencyinjection . Models puЬlic class MemoryRepository : IRepository private Dictionary<string , Product> products; puЬlic MemoryRepository() { products = new Dictionary<string , P r oduct>() ; new List<Product> { new Product { Name "Kayak", Price = 275 М }, new Product { Name = " Lifejacket ", Price = 48 . 95 М }, new Product { Name = "Soccer ball", Price = 19.50 М) }. ForEach (р => AddProduct (р) ) ; puЫic IEnumeraЬle<Product> Products => products.Values; puЬlic Product this[string name] => products [ name] ; puЫic void AddProduct(Product product) => products[product.Name] = product ; puЬlic void DeleteProduct(Product product) => products . Remove(product . Name) ;
Глава 18 . Внедрение зависимостей 547 Класс MemoryReposi tory хранит свои объекты модели в памяти , используя сло­ варь. Это значит, что постоянное хранилище отсутствует, и останов либо перезапуск приложения сбросит модель к примерам объектов данных, которые создаются в конс­ трукторе. В реальном про екте такой подход нецелесообразен, но его будет достаточ­ но для настоящей главы, внимание которой сосредоточено на другом аспекте работы приложения. Создание контроллера и представления Создайте папку Controllers и добавьте в нее файл класса по имени HomeC o ntroller . cs с содержимым, показанным в листинге 18.6 . Листинг 18.6 . Содержимое файла HomeController.cs из папки Controllers using Microsoft . AspNetCore . Mvc ; namespace Dependencyinjection . Contro llers puЫic class HomeController : Controller puЫic ViewRes ult Index() => View (); Контроллер Ноте имеет только один метод действия, применяющий метод Vi ew () для создания объекта ViewResu l t, который будет визуализировать стандартно е представление . Чтобы создать пр едставление, ассоциированное с методом действия, создайте папку Views/Home и добавьте в нее файл представления Razor по имени Index . cshtml . Разметка, помещаемая в это представление, приведена в листинге 18.7 . Листинг 18.7 . Содержимое файла Index.cshtml из папки Views/Home @model IEnumeraЫe<Product> @{ Layout = null ; < ! DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=de v ic e-width " /> <title>Dependency Injection</title> <link rel= "stylesheet " asp-href - include= "li Ь/bootstrap/dist/css/* .m in . css " /> </head> <body class= "panel-bod y" > @if (ViewData .C ount > 0) <tаЫе class= " taЫe taЬle - bordered taЫ e - condensed taЫe - striped"> @foreach (var kvp in ViewData) { <tr><td>@kvp . Key</td><td>@kvp . Va lu e</td></tr> } </tаЫе> <tаЫе class= " taЫe taЫe-bordered taЬle - co n densed ta Ьl e -strip ed " > <thead> <tr><th>Name </th><t h > P rice</th ></t r > </thead> <tbody> @if (Model == n ull) { <tr><td colspan= " З " class= "text - center " >No Model Data</td></tr> }else{
548 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC @foreach (var р in Model) <tr> <td>@p . Name</td> <td>@str i ng . Format ("{ O:C 2 }", p . Price)</td> </tr> </ t body> </tаЫе> </body> </html > Представление строго типизировано с использованием пер е числения объектов Pro d u ct , и основны м содержимым представления является НТМL-таблиц а . Если контроллер не пр едоставляет какие-либо данные модели, тогда в таблице будет отоб­ ражаться только соответствующее сообщение. При наличи и данных модели для каж­ дого объекта Product и з перечисле н ия к таблице добавля ется строка. Им ее тся таюк е таблица, в которо й будут перечи слены ключи и зн ачения из объ екта ViewBag , если они есть; в противном случае она будет скрыта. Данная табл ица будет задействована позже в главе. При стилизации НТМL-элементов представление полагается на п акет CSS из Bootstrap. Чтобы добавить Bootstrap в проект, созда йте файл bower . j son с при м ене­ нием шаблона элемента Bower Configuration File (Файл конфигурац ии Bower) и добавь­ те пакет Bootstrap (листинг 18.8). Листинг 18.8. Добавление пакета Bootstrap в файле bower. j son "name": "asp.net", "pri vate": true, " dependencies ": { "bootstrap": "3.3.6" Последний подготовительный шаг предусматривает со здание в паш'е Views фай­ ла _View i mp orts . csht ml, который устанавливает вс т роенны е дескрип торные вспо­ могательные классы для использования в представлениях Razor и импортирует про­ странство имен модели (листинг 18.9). Листинг 18.9. Содержимое файла_Viewimports. cshtml из папки Views @using Dependen cy inj ection . Models @addTagHelpe r * , Microsoft . AspNetCo r e . Mvc . TagHelpers Создание проекта модульного тестирования Добавьте в решение Visual Studio папку te s t и с применен ие м шаблона Class Library (.NET Core) (Библиотека классов (.NET Core)) создайт е проект по и мени Dependencyinj ecti on. Te sts. следуя процессу, который был описан в главе 7. Замените стандартное содержимое файла proj e ct . j s on конфигурацией, прив еден­ ной в листинге 18 . 10.
Глава 18 . Внедрение зависимостей 549 Листинг 18.1 О. Содержимое файла proj ect . j son из проекта Dependencyinjection . Tests "version": "1 .0 .0-*", " testRunner ": " xuni t ", "dependencies ": { "Microsoft . NETCore . App ": " type": " platform", "version": "1 .0 .0" }, "xunit": "2 .1 .0", " dotnet - test-xunit ": " 2 . 2 . 0 - preview2 -bui ld1029 ", "D ependencyinjection ": "1. 0 . 0" , }, "moq . netcore" : "4.4 .0-betaB ", " System . Diagnostics . TraceSource ": "4.0 .0" " frameworks ": { " netcoreappl . 0 ": " imports ": [ "dotnet5 . 6 ", "p orta Ьle-net 45+win8 "] Запустив приложение , вы увидите результат, показанный на рис. 18.1 . С (_2._locall1~:_s_9_4_39___ --~ Name Price No Model Data L____" ___· ··--·-- ---- Рис . 18.1. Запуск примера прило же ния Создание слабо связанных компонентов Как видно на рис. 18.1 . данные модели при запуске приложения отсутствуют. Причина в том, что не установлено отношение между классом HomeController . который должен передавать данные модели своему представлению, и классом MemoryReposi tory, содержащим данные модели. Целью соединения вместе компо­ нентов в приложении MVC является обеспечение возможности легк ой замены компо­ нента альтернативной реализацией той же самой функциональности. Наличие возможно сти замены компонентов позволяет эффективно проводить мо­ дульное тестирование, облегчает изменение поведения приложения в разных средах размещения (таких как сервер разработки и производственный сервер) и упрощает долгос рочно е сопровождение приложения.
550 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC В последующих разделах начнете.я объяснение альтернативного подхода и про ­ блем, которые он привносит. Это может выглядеть как непрямой способ объяснения средства внедрения зависимостей, но одна из сложностей , присущих DI. заключает е.я в том , что Dl решает проблему, которая не всегда очевидна при написании кода и про.являете.я только на более поздней стадии цима разработки. Мнение о внедрении зависимостей Внедрение зависимостей - одна из тем, по поводу которых читатели связываются со мной наиболее часто. Примерно в половине сообщений электронной почты выражается недо­ вольство относительно того, что я "навязываю" 01. Как ни странно, во второй половине со­ общений сетуют на то, что я недостаточно сильно делаю акцент на преимуществах 01, и другие читатели могут не осознать, насколько полезным способно быть это средство. Внедрение зависимостей может оказаться сложной для понимания темой, и ценность его спорна. Средство DI может быть удобным инструментом, но далеко не всем оно нравится и не всем требуется. Средство 01 приносит лишь ограниченную пользу, если вы не проводите модульное тести­ рование или выполняете небольшой, самодостаточный и устойчивый проект. Понимать, как работает 01 , по-прежнему полезно, поскольку DI используется для доступа к ряду важных средств MVC, но вы далеко не всегда обязаны применять 01 в своих контроллерах и других классах. Я использую DI в своих проектах главным образом потому, что проекты часто двигаются в неожиданных направлениях, а наличие возможности легкой замены компонента новой реа­ лизацией может уберечь от внесения большого числа утомительных и чреватых ошибками изменений. Я предпочитаю приложить определенные усилия в начале проекта , чем иметь дело со сложным набором правок на более позднем этапе. Я совершенно не категоричен в отношении внедрения зависимостей, т.к. оно решает проблему, которая возникает не в каж­ дом проекте. Только вы можете решить, нужно ли DI в вашем проекте, и только вы способны оценить получаемые выгоды и сопутствующие затраты. И сследование сильно связанных компонентов Естественной склонностью большинства разработчиков .является выбор наиболее прямого пути к решению задачи. В примере приложения это озн ачает применение мюч евого слова new для создания объекта хранилища, который требуется контрол­ леру, чтобы получить данные модели (листинг 18.11). Листинг 18.11. Создание объекта хранилища в файле HomeCon troller. cs using Microsoft.AspNetCor e .Mvc ; using Dependencyinjection.Models; namespace Depende ncyinj ection. Controlle r s puЬlic cl ass HomeController : Controller puЬlic ViewResult Index() => View(new MemoryRepository() . Products); Хорошая новость об этом коде состоит в том, что он работает. Запустив приложе ­ ние, вы увидите детали объектов модели, отображаемые в браузере (рис. 18.2).
CJ Dep1mdency lnj ecti o n Х - С [<D l~all1ost:59439 Name Kayak Lifejacket Soccer ball Глава 18. Внедрение зависимостей 551 Price $275.00 $48.95 $19.50 -----·------------··--------- Рис. 18.2. Отображение данных модели Плохая новость в том, что контроллер Н оте и класс Meтor yRe p osi t o r y теперь сильно связаны, т.е. заменить хранилище , не изменяя класс HoтeCo n t r o ll e r, не удас­ тся. Как объяснялось в главе 7, выполнение эффективного м одульного тестирования означает возможность изоляции одиночного компонента , но протестировать метод действия Index () в листинге 18.11 невозможно бе з неявного тестирования также и класс а хранилища. Если модульный тест не пройдет, то нельзя будет утверждать, где конкретно присутствует проблема - в контроллере, хранилище или каком-то другом компоненте, от которого зависит хранилище . Во всех практических смыслах конт­ ролл ер Ноте и класс Meтo ryRep o si t o r y формируют единственный индивидуальный модуль, как иллюстрируется на рис. 18.3 . 1Ко"'ролмр Н Хр'"илище Рис. 18.З. Эффект от сильно связанных компонентов Развязывание компонентов для модульного тестирования В главе 7 использовалось свойство для хранения ссылки на класс хранилища через реализуемый им интерфейс . что позволило создавать имитированное хранилище для целей модульного тестирования. В листинге 18.12 такой подход применяется к конт­ роллеру в текущем прим е ре приложения . Листинг 18.12. Использование свойства для хранилища в файле HomeController . cs using Microsoft. AspNetCo r e .Mv c ; using De pe nde ncyi nject i on.Model s; names pace Dependencyi njecti on.Cont r ollers puЫic cl ass HomeControl l er : Controller ) puЫic IRepository Repository { get; set; } = new MemoryRepository(); puЫic ViewResul t Index () => View (Reposi tory. Products) ;
552 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Этот прием прекрасно подходит, если вы хотите провести модУльное тестирование, т.к. он позволяет изолировать класс контроллера, устанавливая свойство Repos i tory перед вызовом метода действия внутри модульного теста. Добавьте в проект Dependencyinj ection . Test s файл класса по имени DI Tests. cs и определите в нем модульный тест, показанный в листинге 18.13, J{О­ торый применяет свойство Repository для настройки фиктивного хранилища перед взаимодействием с контроллером. Листинг 18.1 З. Тестирование контроллера в файле DITests. cs внутри проекта Dependencyinjection. Tests using Dependency inj ection.Cont r ol l ers ; using Dependencyinjection.Models; using Microsoft . AspNetCore . Mvc; using Moq; using Xunit; namespace Tests puЫic class DITests [Fact] puЬlic void ControllerTest() 11 Организа ция var data = new [] { new Product { Name = "Test", Price var mock = new Mock<IRepository>(); mock.SetupGet(m => m.Products) .Returns(da ta); Hom eCont roller controller = new HomeController Repository = mock . Object }; 11 Действие ViewResult result = controller.Index(); 11 Утверждение Assert.Equal(data , result . ViewData.Model); 100}}; Свойство Repository позволяет изолировать контроллер и предоставить тестовые данные, которые можно инспектировать в объекте ViewResul t, созданном методом действия. В итоге мы обеспечиваем только частичное решение проблемы сильно связан­ ных компонентов, потому что установить свойство Reposi tory, когда приложение фун­ кционирует. невозможно. В главе 17 было указано, что инфраструктура МVС отвечает за создание э1<земпляров контроллеров для обработки запросов, но ей ничего не известно об особой важности, придаваемой свойству Reposi tory. Результатом такого приема является то. что контроллер и хранилище слабо связаны при проведении модульного тестирования и сильно связаны во время вьmолнения приложения (рис. 18.4). Использование брокера типов Следующий логический шаг предусматривает вынесение кода, в котором решает­ ся, какую реализацию интерфейса хранилища применять, за пр еделы класса контрол­ лера и его помещение куда-то в другое место приложения. Чтобы посмотреть, как это может выглядеть, добавьте в проект приложения папку Infrastructure и создайте в ней файл класса по имени TypeBroker . cs с содержимым из листинга 18.14.
Глава 18. Внедрение зависимостей 553 Тестирование Контроллер " Хранилище Развертывание 1Ко"'роллер Н И"'ерфейс Н ХР'"'"'ще Рис . 18.4 . Эффект от добавления свойства Reposi tory Листинг 18.14 . Содержимое файла ТypeBroker.cs из папки Infrastructure using Dependencyi njection.Models; using System; narnespace Dependencyinjection.Infrastructure puЫic static class TypeBroker { private static Туре repoType = typeof(MemoryRepository); private static IRepository testRepo ; puЫic static IRepository Repository => testRepo ?? Activator .Cre ateinstance(repoType) as IRepository; puЫic static void SetRepositoryType<T>() where Т: IRepository => repoType = typeof (Т); puЫic static void SetTestObject(IRepository repo) { testRepo = repo ; В классе TypeBroker определено свойство Reposi tory , которое возвращает но­ вые объекты, реализующие интерфейс IReposi tory. Класс реализации, использу­ емый свойством Reposi tory, определяется значением поля repoType, которое по умолчанию установлено в MemoryReposi tory, но может быть изменено вызовом ме­ тода SetReposi toryType () . Для поддержки модульного тестирования метод SetTestObj ect () позволяет при­ менять специфический объект. В листинге 18.15 контроллер обновлен, чтобы полу­ чать объект хранилища от брокера типов. Листинг 16.15. Использование брокера типов в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using Dependencylnjection . Models ; using Dependencyinjection.Infrastructure; name space Dependencyinjection.Controllers puЫic class HomeControl l er : Controller puЫic IRepository Repository { get; } = TypeBroker.Repository; puЫic ViewResult Index() => View(Repository . Products) ;
554 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Теперь в примере приложения имеется более сложный набор отношений, как де­ монстрируется на рис. 18.5 . Важно отметить, что между классом контроллера и клас­ сом хранилища никакого прямого отношения нет - все реш ается при посредстве интерфейса и брокера . Это значит, что класс хранилища можно изменять без необхо­ димости во внесении изменений в класс контроллера. Контроллер "'- Интерфейс ~ Хранилище .. ... ~ Брокер ~ Рис. 18.5. Эффект от добавления брокера типов Чтобы посмотреть, как применя ется брокер типов , добавьте в папку Models файл класса по имени Al ternateRepos i tory . c s с еще одной реали з ацией интерф е йса IRepos i tory , приведенной в листинге 18.16. Листинг 18.16. Содержимое файла Al ternateReposi tory. cs из папки Model s using System . Co ll ections . Generic ; names p ace Depe n dencyinjection . Models puЫic cl ass Al ternateReposi tory : IReposi tory p ri vate Dictionary <string , Produ c t> product s; puЫ ic AlternateRep ository ( ) { products = new Dictionary<str i ng , Pr oduct> () ; new List<Product> { new Product {Name = "Corner Flags", Price = 34 .95 М }, new Product { Name = " Stadium", Price = 79500 М } }. Fo rEach (р => AddProduct (р)) ; puЫic IEnumeraЫe<Product> Products => pr oducts . Val ues; puЫic Product t his [string name ] => p r oducts[name ] ; puЫ i c void AddProduct( Produc t product) => product s[product . Name] = p r oduct; puЫic void Delete P roduct( Product product) => p roducts . Remove( pr oduct . Name ); В реальном приложении альтернативное хранилище могло бы хранить данные в другом формате либо использовать постоянство другого вида. В текущем приме ­ ре отличие между классами A l terna te Re pository и MemoryRepository каса ­ ется данных модели, которые они создают при создании их экз е мпляров. Для при­ менения класса Al te rn at e Reposito ry сконфигурируйте брокер типов в м етоде Confi g u reServ i ces () класса Startup, как пок азано в листинг е 18 . 17.
Глава 18 . Внедрение зависимостей 555 Листинг 18.17 . Конфигурирование брокера типов в файле Startup. cs usi ng Microsoft .AspNetCore.Builder; using Microsoft . Extensions . Dependencyi njection ; using Dependencyinjection.Infrastructure; using Dependencyinjection.Models; namespace Dependencyinjection puЫic class Start up { puЫic void Confi gureServi ces(ISe r viceCol l ection services) TypeBroker.SetRepositoryType<AlternateRepository>(); services . AddМvc() ; puЬlic void Con figu re( IAp pl icatio n Builde r арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app .UseSt aticFil es() ; app . UseMvcWi thDefaul tRoute() ; Результат этого изменения можно увидеть, запустив приложение, которо е отобра­ зит данны е , предоставля емые новым I<Лассом хранилища (рис . 18.6) . [J Dependency lnjectio n 1------С-~- 1oc-;;-111o~:s9'439 Name Corner Flags Stadium Price $34.95 $79,500.00 L___________"________ __" .____ _____ _____ __. __ Рис. 18.6 . Изменение класса хранилища Брокер типов по зволяет использовать специфический объект в качеств е хранили ­ ща, что делает возможным написание модульных тестов , подобных представленному в л истинге 18.18. Листинг 18.18. Тестирование контроллера через брокер в файле DITests. cs using Dependen cyinj ect ion . Co n troll ers ; using Dependencyinject i on .I n f ra s truct u re ; using Dependencyin j ection . Mo d els ; using Microsoft.As p Ne tCore . Mvc ; using Moq ;
556 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC using Xunit ; n amespace Tests puЫic class DITests [Fact] puЬlic void Contro l lerTest() 11 Организация var data = new[] { new Product { Name = "Test", Price var mock = new Mock<IReposit ory>{); mock . SetupGet (m => m . Products) . Returns(data) ; TypeBroker.SetTestObject(mock.Object); HomeController controller = new HomeController(); 11 Действие ViewResu l t resul t = controller . Index() ; 11 Утверждение As s ert . Equal(data , result. Vie wD ata . Model) ; Введение в средство внедрения зависимостей ASP.NET 100}}; В пр едыдущем разделе вы ознакомились с процессом отделения класса контролл е ­ ра от хранилища. которое поставляет данные модели. Теперь класс HomeContro ller может получать реализацию интерфейса I Re p o s itory, ничего не зная о том. какой rшасс применяется или I{ак создается его эr<земпляр. Знание о классе реализации IReposi tory сод е ржится в кл а ссе TypeBroker . который может испо л ьзовать ся лю­ бым другим контроллером. нуждающимся в доступе к хранилищу. а также для при­ менения тестового объекта . Общим результатом явля ется более гибкое приложение , но остались еще некото­ рые шероховатости. Самый крупный недостаток связан с тем, что для каждого но ­ вого типа, которым должен управлять брокер. пон адобится добавлять новые методы и свойства. Можно было бы пер еписать класс TypeB r o ke r , сделав е го бол е е уни­ версальным. но в этом нет необходимости. поскольку инфраструктура ASP.NET Core предлагает изящную версию той же самой функциональности , пакетированную та­ ким способом, который облегчает ее применение и не требует ник аких специальных классов. Подготовка к внедрению зависимостей Термин вн едрение зависимостей (DI) описывает альтернативный подход к созда ­ нию слабо связанных компонентов, который интегрирован в ASP.NET Core и исполь­ зуется инфраструктурой МVС автоматически, а это означает, что контроллеры и дру­ гие компоненты не обязаны знать, каким образом создаются требующиеся им типы . В листинге 18.19 продемонстрирована подготовка контроллера Home к внедрению зависимостей.
Глава 18 . Внедрение зависимостей 557 Листинг 18.19. Подготовка к внедрению зависимостей в файле HomeCon troller. cs using Microsoft.AspNetCore.Mvc ; using Dependencyinjection.Models; using Dependencyinjection .I nfrastructure ; namespace Dependencyinjection.Controllers puЫic class HomeController : Controller private IRepository repository; puЫic HomeController(IRepository repo) { repository = repo; puЫic ViewResul t Index () => View (repository. Products) ; } Контроллер объявляет свои зависимости в виде аргументов конструктора. Таким образом, здесь учитывается вторая часть термина: зависимости во "внедрении зави­ симостей" - это объекты, которые требуются для создания нового экземпляра класса. В рассматриваемом случае класс контроллера объявляет зависимость от · интерфейса IReposi tory. В инфраструктуре ASP.NET Core компонент под названием поставщик служб от­ вечает за отображение интерфейсов на типы реализации, которые применяются для удовлетворения зависимостей. Когда требуется новый контроллер, инфраструктура MVC запрашивает у постав­ щика служб создание нового экземпляра класса HorneController. Поставщик служб инспектирует конструктор HorneController для выяснения его зависимостей, созда­ ет требуемые объе1пы служб и внедряет их внутрь конструктора, чтобы создать но­ вый контроллер, который может использоваться для обработки запроса. Это основной процесс внедрения зависимостей, поэтому для ясности его стоит повторить. 1. Инфраструктура МVС получает входящий запрос к методу действия в контрол­ лере Horne. 2. Инфраструктура МVС требует у компонента поставщика служб ASP.NET новый экземпляр класса HomeController . 3. Поставщик служб инспектирует конструктор HomeController и обнаруживает, что он имеет зависимость от интерфейса IRepository. 4. Поставщик служб обращается к своим отображениям , чтобы найти класс ре­ ализации, который подлежит применению для зависимостей от интерфейса IRepos i tory. 5. Поставщик служб создает новый экземпляр найденного класса реализации. 6 . Поставщик служб создает новый объект HomeController, используя объект ре­ ализации в качестве аргумента конструктора. 7. Поставщик служб возвращает созданный объект HomeController инфраструк­ туре MVC, который она прим еняет для обработки входящего НТТР-запроса. В целом результат будет таким же, как в случае использования специального клас­ са брокера типов, но важное преимущество заключается в том, что процесс внедре­ ния зависимостей интегрирован в МVС, т.е. компонент поставщика служб будет при-
558 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC меняться всякий раз, когда создается экземпляр класса контроллера. Таким образом , классам контроллеров разрешено объявлять зависимости, не имея никакого понятия о том, как они будут распознаваться. Вы просто пишете классы контроллеров, кото ­ рые объявляют свои зависимости как параметры конструкторов, и позволяете инфра­ структуре вместе с компонентом поставщика служб позаботиться об остальном. На заметку! Во всех примерах в этой главе используется встроенная система внедрения зависимостей, являющаяся частью ASP.NET Саге . Существуют сторонние пакеты, которые допускается применять в качестве замены встроенной функциональности и которые могут предлагать усовершенствования и дополнительные средства . В число популярных пакетов входят Autofac и StгuctuгeMap, хотя на время написания книги для их интеграции в ASP.NET Саге требовались дополнительные па кеты. Подробные сведения доступны по адресу http://github . com/aspnet/Dependencyinjection/ЫoЬ/dev/README.md, но перечисленные там па ке ты, скорее всего, выйдут из употребления по мере интегриро­ вания поддержки ASP.NET Core в главные пакеты 01. Конфигурирование поставщика служб Объявление зависимости через конструктор HomeController нарушает рабо­ ту приложения, в чем легко удостовериться, запустив проект. Когда MVC пытается создать экземпляр класса HomeControl ler для обслуживания запроса, возникает ошибка, показанная на рис. 18.7 . ~ lnter·мl Serve1 Error Х '----- С lф-=-~~~~-------=-----~~-===-~=о~~} _ _: -11 ' 1 Ап unhandled exception occurred while pгocessing the request. J lпva li clOpeгationExceptio11: Unable to гeso lve seгvice for type j 'Depeпdeпcylnjectio11.Models.IReposito1·y' \Vhile atte rпp<iпg to activate / 1 'Depeпdeпcylп1ectюп.Co пёrollers . HorпeCoпtro!ler'. i j Microsoft.Extensions.lnterna l.Actf.1a tcrU tilrties.GetService(( St>rvkeProvider sp. Туре type-, Турё' requiredBy. Boolean 1 1 isDefaul t Para meterRequired) j ! Query Cookies Heade1s 1 1 1 !Nol]~?1pe1~qti~x~tif~~~T~'~~~#,,,-_..~ Рис. 18. 7 . Запуск примера приложения Для распознавания зависимостей поставщик служб должен быть сконфигурирован так, чтобы он знал , каким образом распознавать зависимости от служб. В настоящий момент поставщик служб не располагает такой информацией и генерирует исключе ­ ние, когда ему предлагается создать объект HomeController, потому что ему ничего не известно о том, как распознавать зависимость от интерфейса IReposi tory. Конфигурация для поставщика служб определяется в классе Startup, так что служба будет на месте к моменту запуска приложения для получения запросов. В лис­ тинге 18.20 поставщик служб конфигурируется так , чтобы ему было известно, каким образом иметь дело с зависимостями от интерфейса IReposi to ry.
Глава 18. Внедрение зависимостей 559 Листинг 18.20. Конфигурирование поставщика служб в файле startup. cs using Microsoft . AspNetCore .B uilder ; using Microsoft.Extensions.Dependencyinjection ; using Dependencyinjection.Infrastructure; using Dependencyinjection . Models ; namespace Dependencyinjection puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) services.AddTransient<IRepository, MemoryRepository>(); services.AddMvc() ; puЬlic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvcWithDefaultRoute() ; Внедрение зависимостей конфигурируется с использованием расширяющих мето­ дов, которые вызываются на объекте реализации IServiceCollection, полученном с помощью метода ConfigureServices () .Расширяющий метод AddTransient (), который применялся в листинге 18.20, сообщает поставщику служб, как обрабаты­ вать зависимость (он будет описан более подробно позже в главе). Отображение выра­ жается с использованием параметров типов, где первый тип является интерфейсом, а второй - классом реализации. services . AddTransient< IRepository, MemoryRepository>() ; Этот оператор указывает поставщику служб о том, что распознавать зависимости от интерфейса IReposi tory необходимо путем создания объекта MemoryReposi tory. Запустив приложение , вы увидите, что зависимость, объявленная конструктором HomeController, распознана и контролл е ру предоставлен доступ к данным модели (рис. 18.8). D D•pendency lnjection Х .--- с [_SO~lhost:59439 ,-,........__---~--,----~--=-=~ Name Kayak 1 1 Lifej acket Soccer ball !____ Price $275.00 $48.95 $19.50 ·------------------~ Рис. 18.8. Конфигурирование внедрения зависимостей
560 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Модульное тестирование контроллера с зависимостью Применение конструктора для получения зависимостей облегчает модульное тес­ тирование контроллеров. В листинге 18.21 приведен модульный тест для контроллера из листинга 18.20. Листинг 18.21. Тестирование контроллера в файле DITests. cs внутри проекта Dependencyinjection. Tests using Dependencyinjection.Controllers; using Dependencyinjection.Models; using Microsoft.AspNetCore.Mvc; using Моq; using Xunit; namespace Tests puЫic class DITests [Fact] puЫic void ControllerTest() 11 Организация var data = new[] { new Product { Name = "Test", Price 100} } ; var mock = new Мock<IRepository> (); mock . SetupGet(m => m.Products) . Returns(data); HomeController controller = new HomeController(mock.Object); 11 Действие ViewResult result = controller.Index(); 11 Утверждение Assert.Equal(data , result.ViewData . Model); Контроллер ничего не знает, да и не заботится о том, какой вид объекта передает­ ся конструктору, до тех пор, пока он реализует корректный интерфейс. Это позволяет использовать фиктивное хранилище, не полагаясь на какой-нибудь внешний класс, такой как брокер типов, что может повлиять на исход теста. Использование цепочек зависимостей Когда поставщику служб необходимо распознать зависимость, он инспектирует тип, который был сконфигурирован для применения, чтобы выяснить, имеет ли он в свою очередь зависимости, подлежащие распознаванию. Результатом является воз­ можность создания цепочки зависимостей, которые все распознаются во время вы­ полнения и могут управляться поср едством конфигурации в классе Startup. Чтобы посмотреть на образование цепочки зависимостей, добавьте в папку Models файл класса по имени IМodelStorage. cs с определением интерфейса, представленным в листище 18.22. Листинг 16.22 . Содержимое файла IModelStorage. cs из папки Models using System . Collections.Generic; namespace Dependencyinjection . Мodels puЫic interface IModelStorage { IEnumeraЫe<Product> Items { get;
Product t his [string key] { get; set; } boo l ContainsKey(string key) ; void Removeitem(string key); Глава 18 . Внедрение зависимостей 561 Интерфейс IModelStorage определяет поведение простого механизма хранилища для объектов Produc t. Для реализации этого интерфейса добавьте в папку Models файл класса по имени DictionaryStorage . cs с определением из листинга 18.23. Листинг 18.23. Содержимое файла DictionaryStorage. cs из папки Models using System . Co llections.Ge n eric ; namespace Dependencyinjection.Models puЫic c l ass DictionaryStorage : IM o d e lSto r age private Dictionary<string , Produ c t > items = new Dictionary<string , Prod u ct> (); p u Ьlic Product this [ str ing key] get { return i tems [key] ; } set { items[key ] = value ; } puЬlic IEnumeraЫe<Product> I tems = > items . Values ; puЫic bool ContainsKey(string key) = > items . Conta in sKey(key) ; puЬlic void Removeitem(string key) = > items.Remove(key); Класс DictionaryStorage реализует интерфейс IModelStorage, используя стро­ го типизированный словарь для хранения объе1пов модели. Такая функциональность в текущий момент содержится внутри класса MemoryReposi tory, и отделение с при­ менением интерфейса имеет малую ценность в реальном проекте, но мы получаем по ­ ле зный прим ер того, как внедр ение зависимостей может использоваться, не привнося слишком много дополнительной сложности в приложение. В листинге 18.24 класс MemoryReposi tory модифицирован так, что он объявляет зависимость от интерфейса IModelStorage , но без какого -либо знания о классе реа­ лизации, который будет применяться во время выполнения. Листинг 18.24. Объявление зависимости в файле MemoryReposi tory. cs using System . Collections . Gener i c ; namespace Dependencyinjection . Models puЫic class MemoryRepos i tory : IRepos itory private IModelStorage storage; puЫic MemoryRepository(IModelStorage modelStore) storage = modelStore; new List<Product> { new Produ c t Name "Kayak ", Price = 275 М ), new Product { Name " Lifejacket", Price = 48. 95 М }, new Product { Name " Soccer ball ", Price = 19.50 М} } . ForEach(p => AddProduct(p)) ;
562 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC puЬlic IEnumeraЫe<Product> Products => storage. Items; puЫic Product this[string name] => storage[name]; puЫic void AddProduct(Product product) => storage[product.Name] = product; puЫic void DeleteProduct(Product product) => storage.Removeitem(product.Name); Запустив приложение , вы увидите, что поставщик служб сгенерирует исключение со следующим сообщением : InvalidOperationException : UnaЬle to resolve service for type ' Depend encyinjection . Models .I ModelStorag e' while attempting to activate ' Dependencyin j ection.Models . MemoryRepository '. InvalidOpera tionException : не удается распознать службу для типа Dependencyin je ction .Models . IModelStorage при попытке активации Dependencyinjection . Models . MemoryRepository. Это демонстрирует прохождение поставщика служб через цепочку зависи мос­ тей. Когда у н его было затребовано создание нового объекта контроллера, он про­ инспектировал конструктор HomeController и нашел зависимость от интерфей­ са I Repos i tory , которая, как ему и з вестно, должна распознаваться посредством объекта MemoryRepos i tory. Затем поставщик служб исследовал конструктор MemoryRepos i to r y , который имеет зависимость от интерфейса IModelStorage. Но в конфигурации не ука зано, каким образом должны распознаваться зависимости от IModelStorage . т.е. объект Me mo ryRepository не может быть создан, равно как не может быть создан и объект HomeController. Поставщик служб не в состоянии пре­ доставить инфраструктуре MVC объект, необходимый для обработки запроса, потому было сгенерировано исключение. Необходимо добавить отображение типа , которое сообщит поставщику служб о том, I<а к должны распознаваться зависимости от I Mode lStorage, что и делается в конфигурации приложения, показанной в листинге 18.25. Листинг 18.25. Конфигурирование дополнительного отображения типа в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions . Dependencyinjection ; using Dependencylnjection . Inf r astructure ; using Dependencyinjection.Models; namespace Dependencyinjection puЬlic class Startup { puЫic void Conf i gureServi ces(IServiceCollect i on services) { services.AddTransient < IRepository , Me moryRepository>() ; services.AddTransient<IModelStorage, DictionaryStorage>(); services .AddMvc();
Глава 18 . Внедрение зависимостей 563 puЫic void Configure(IApplicat i onBuilde r арр) [ app . UseStatusCodePages() ; app . OseDeveloperExceptionPage() ; app . OseStaticFi l es() ; app . OseMvcWithDefaultRoute() ; С таким добавлением поставщик служб может удовлетворить обе зависимости в це­ почке и способен создать набор объектов, требуемых для обслуживания запроса: объек­ та DictionaryStorage, внедряемого внутрь конструктора класса Memo r yReposi tory, объект которого внедряется внутрь конструктора HomeController . Цепочки зави­ симостей - не просто искусный трюк; они позволяют формировать сложную функ­ циональность, объединяя компоненты, которые могут быть легко изолированы для тестирования и очень просто изменены, чтобы удовлетворять развивающимся требо ­ ваниям проекта по мере обретения им зрелости. Использование внедрения зависимостей для конкретных типов Внедрение зависимостей может также применяться для конкретных типов, доступ к которым не производится через интерфейсы. Несмотря на то что здесь не обеспе­ чиваются преимущества слабой связанности, присущие использованию интерфейсов, сам по себе это удобный прием, поскольку он позволя ет получать доступ к объектам где угодно в приложении и помещает конкретные типы под управление жизненным циклом, как описано далее в главе. Добавьте в папку Models файл l(Jlacca по имени ProductTotalizer. cs с сод е р­ жимым, приведенным в листинге 18.26. Листинг 18.26. Содержимое файла ProductTotalizer. cs из папки Models using Systern . Linq ; namespace Dependencyinjection . Mode ls puЫic class ProductTotalizer [ puЬlic ProductTota li zer(IRepositor y repo) { Repository = repo ; puЬlic IReposito r y Repository { get ; set ; } puЫic decimal Total => Repository.Products . Surn(p => p . Pr i ce) ; Класс ProductTota liz er не дела ет ничего особо полезного, но имеет зависимость от интерфейса IReposi tory, а это значит, что прим енение внедрения зависимостей распозна ет данную зависимость, используя конфигурацию, которая также применя­ ется к остальной части приложения. В листинге 18.27 показано объявление класса ProductTotalizer как зависимости для класса Ho meContro ll e r.
564 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 18. 27. Добавление зависимости в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using Dependencyinjection . Models ; using Dependencyinjection . Infrastructure ; namespace Dependencyinjection . Contro l lers puЫic class HomeController : Controller private IRepository repository; private ProductTotalizer totalizer; puЫic HomeController(IRepository repo, ProductTotalizer total) { repository = repo; totalizer = total; puЫic ViewResult Index() ViewBag.Total = totalizer.Total; return View(repository.Products); Действие I ndex добавляет свойство ViewBag , содержащее обшую сумму, выпус­ каемую классом ProductTotalizer, которая отобразится в таблиц е для значений ViewBag , добавленной к представлению I ndex . cshtml в начале главы. Финальный шаг заключается в сообщении поставщику служб о том, как поступать с запросами ProductTotalizer (листинг 18.28). Листинг 18.28. Конфигурирование поставщика служб в файле Startup. cs puЫ i c void Con figu reServices(IServiceCollection services) { services . AddTransient<IRepository , MemoryReposi tory>() ; services . AddTransient< I ModelStorage , DictionaryStorage>() ; services.AddTransient<ProductTotalizer>(); services . AddMvc() ; В данной ситуации отсутствует отображение между типом службы и типом р е­ ализации , поэтому используется перегруженная версия расширяюще го метода AddT r ansient (), принимающая единственный параметр типа, который сообщ ает поставщику служб. что для распознавания зависимости указанного типа он должен создавать экземпляр класса ProductTotalizer . Преимущества такого подхода - в противоположность простому созданию экземп­ ляра конкретного класса внутри контроллера - связаны с тем, что поставщик служб будет распознавать любые зависимости, объявляемые конкретным классом, а также с тем, что появляется возможность изменять конфигурацию для распознавания зависи­ мостей от конкретного класса с применением более специализированных подмассов . Конкретные классы управляются поставщиком служб и подвергаются воздействию со стороны средств жизненного цикла, которые рассматриваются в следующей главе . Запустив приложение, вы увидите, что оно отображает обшую сумму для объектов Product в модели (рис . 18.9) .
Глава 18. Внедрение зависимостей 565 D Dep~ndency lnjection Х j С [ CD ~:all~ost59~3!_ ==-===-- -- -·-- *-/ ,_ Total 343.45 i J Name Price i~ il!!!1fjiilt_ . . . . , .41>. $275.00 ......_..,..."6,- . ~~ Рис. 18.9. Использование внедрения зависимостей для классов Жизненные циклы служб В предыдущем разделе для сообщения поставщику служб о том, как он должен обрабатывать зависимости от интерфейсов IRepository и IModelStorage, приме­ нялся расширяющий метод AddTransient () .Метод AddTransient () - один из че­ тырех способов определения отображений типов. В табл. 18.3 описаны расширяющие методы, которые указывают поставщику служб, каким образом распознавать зави­ симости. Все методы в табл. 18.3 используют параметры типов, но доступны также расширяющие методы, которые взамен принимают в качестве аргументов объекты Туре, что может быть полезно, если необходимо генерировать отображения во время выполнения. Таблица 18 .З. Расширяющие методы, сообщающие поставщику служб о том, как распознавать зависимости Имя AddTransient<service , implType>() AddTransient<service>() AddTransient<service>(factoryFunc) Описание Этот метод указывает поставщику служб на необходимость создания нового э кземпляра типа реализации для каждой зависимос- ти от типа службы , как описано в разделе "Использование переходного жизненного цик­ ла" далее в главе Этот метод применяется для регистрации одиночного типа, экземпляр которого будет создаваться для каждой зависимости, как объяснялось в разделе "Использование внед­ рения зависимостей для конкретных типов " ранее в главе Этот метод применяется для регистрации фабричной функции, которая будет вызывать­ ся с целью создания объекта реализации для каждой зависимости от типа службы, как по­ казано в разделе "Использование фабричной функции" далее в главе
566 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Имя AddScoped<s ervi ce , implType>( ) AddScoped<ser vi ce>( ) Ad dScoped<servi ce> ( fa c tory Func ) AddSingl eton<ser vice , impl Type >() AddSing l eton<service>() AddSi ngleton <s ervi ce> ( facto ryFunc ) AddSing l eton<se r v i ce>( ins tance ) Окончание табл. 18.3 Описание Эти методы сообщают поставщику о том, что экземпляры типа реализации должны использоваться повторно. Таким образом, все запросы к службе со стороны компонен­ тов, ассоциированных с общей областью действия, которой обычно является оди­ ночный НТТР-запрос, разделяют один и тот же объект. Данные методы следуют тому же самому шаблону, что и соответствую- щие методы Add Trans i e nt (). См. раздел "Использование жизненного ци кла, ограни­ ченного областью действия" далее в главе Эти методы указывают поставщику служб на необходимость создания ново~о экземпляра типа реализации для первого запроса к служ­ бе и затем его повторного использования для всех последующих запросов к службе . См . раздел "Использование жизненного цикла одиночки" далее в главе Этот метод предоставляет поставщику служб объект, который должен применяться для об ­ служивания всех запросов к службе. Поставщик служб не будет создавать новые объекты Использование переходного жизненного цикла Простейший способ приступить к применению внедрения зависимостей предус­ матривает использование метода Add Tran sient ( ) , который сообщает поставщику служб о том, что он должен создавать новый экземпляр типа реализации всякий раз , когда нужно распознать зависимость. Это конфигурация, которая уж е присутствует вклассе Start up: puЫ i c vo i d ConfigureServices(IServi ceCollect i on servi ces) { services .AddTrans1ent< I Repository , MemoryRepository>() ; s ervic e s.AddTransient<IM odelStorage , Di c ti onarySto r a ge> (); services .AddTrans1ent<P r o du ctTotalizer>() ; ser vices . AddMvc() ; Со всеми жизненными циклами, описанными в табл. 18.3, связаны компромиссы. Переходный жизненный цикл влечет за собой затраты по созданию нового экземпляра класса реализации каждый раз, когда распознается зависимость, но его преимущест­ во в том , что не приходится беспокоиться об управлении параллельным доступом или обеспечении безопасного многократного применения объекта множеством запросов. Для демонстрации переходного жизненного цикла в классе MemoryRepository переопределяется метод ToS tring (), чтобы он генерировал глобально уникальный идентификатор (globally unique identifier - GUID), как показано в листинге 18.29.
Глава 18. Внедрение зависимостей 567 Листинг 18.29. Переопределение метода ToString () в файле MernoryReposi tory. cs using System . Collections .G eneric ; namespace Dependencyinjection.Models puЫic class MemoryRepository : IRepository private IModelStorage storage; private string guid = Systern.Guid.NewGuid() .ToString() ; puЫic MemoryReposito ry (IModelStorage modelStore) { storage = modelSto re; new List<Product> { new Product { Name "Kayak", Price = 275 М ), new Product { Name "Lifejacket", Price = 48 .95 М ), new Product { Name " Soccer ball", Price = 19 . 50 М} } .ForEach(p => AddProduct(p)) ; puЫic IEnumeraЫe<Prod uct > Products = > storage.Items; puЫic Product this[st ri ng name] => storage[name] ; puЬlic void AddProduct(Product product) => storage[product.Name ] = product; puЬlic void DeleteProduct(Product product) => storage.Removeitem( pr oduct .N ame) ; puЫic override string ToString() { return guid; Идентификатор GUID облегчит опознание специфического экземпляра класса MemoryRepository и покажет, как различные методы жизненного цикла изменяют поведение поставщика служб. В листинге 18.30 метод действия Index () 1юнтроллера Ноте модифицирован так, чтобы создавать свойство Contro l ler в объекте ViewBag и устанавливать его в GUID из хранилища. Листинг 18.30. Использование объекта ViewBag в файле HomeController. cs using Microsoft . AspNetCo re. Mvc ; using Dependencyinjection . Models; using Dependencyinjection . Infras tructure; namespace Dependencyinjection . Controllers puЫic class HomeCont r o ll er : Contro l ler private IRepository repository; private ProductTotalizer totalizer ; puЫic HomeControlle r ( IRepository repo , ProductTotalizer total) ( repository = repo; totalizer = total; puЫic ViewResult Index() ViewBag.HorneController = repository . ToString(); ViewBag.Totalizer = totalizer.Repository.ToString(); return View(repository . Products);
568 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC Метод действия Index () добавляет значения в объект Vi ewBag, которые содер­ жат идентификаторы GUID для объектов хранилища, получаемых самим конструк­ тором HomeController и посредством конструктора класса Produ ct To t al i zer , что можно увидеть, запустив приложение . Два идентификатора GUID отличаются друг от друга , поскольку поставщик служб был сконфигурирован с помощью метода AddTransient () , т.е. он создает новый объект MemoryReposit ory для распознава­ ния зависимости класса HomeCo n t r o lle r и еще один для класса ProductTotal izer (рис. 18.10). 1 De pendency lnJeclion Х 1+- . . . ;; С' ~-CJ l oca'ih~~ts8з31---··---·-·--·----··--·-- 75; Е J _________: _ __"__;с__._ ________ _.__ _____,_,_ _ с_"••.~:_-' ·"'-----· . -----·---r Ho meControll er 999 4С40З -4d6Ь-4с6а-9З79·С8ае 1 72аЗеd4 ~i. ' rF votalizer 1ij Name Kayak 1+- ""1:" С r 'tl 1;::~~ Ji-~:::;;~"" :::::::~=:~~::~--т1 ·1 1 ~ Tot aliz er 9а9fе37с-504З-4524-9Зfе-1 f8551 7a7068 ' 1 j 1 Name Price 1 ~~....,,... ~'--".... F~~ ..,..~ Рис. 18.1 О. Эффект от переходного жизненного цикла При каждой перезагрузке веб-страницы в браузере новый НТГР- запрос приводит к тому, что инфраструктура MVC создает новый объект HomeContro ller , вызывая создание двух новых объектов Memo r yRe p o sitory , каждый с собственным иденти­ фикатором GUID. Совет. Идентификаторы GUID уникальны (или настолько близ ки к уникальным, что фактичес­ кая разница отсутствует), поэтому при запуске приложения на своей машине вы увидите другие значения. Использование фабричной функции Одна из версий метода Add Tran s i en t () принимает фабричную функцию, кото­ рая вызывается каждый раз, когда встречается зависимость от тип а службы. Это позволяет варьировать создаваемый объ ект, так что разные зависимости получают экземпляры разных типов или экземпляры . которые по-другому с1юнфигурированы. В листинге 18.31 фабричная функция применяется для выбора разных реализаций интерфейса I Repos i tory на основе среды размещения, в которой выполняется приложение .
Глава 18. Внедрение зависимостей 569 Листинг 18.31. Использование фабричной функции в файле startup. cs using Microsoft.AspNetCore.Builder; using Microsoft .Extensi ons . Dependencyinjection; using Dependencyinjection .I nfrastructure ; using Dependencyinjection.Models; using Microsoft.AspNetCore.Hosting; namespace Dependencyinjection { puЫic class Startup { private IHostingEnvironment env; puЬlic Startup(IHostingEnvironment hostEnv) env = hostEnv; puЫic void ConfigureServices(IServiceCollection services) services . AddTransient<IRepository>(provider => { if (env.IsDevelopment()) { } }); var х = provider.GetService<МemoryRepository>(); return х; else { return new AlternateRepository(); services.AddTransient<МemoryRepository>(); services . AddTransient< IMod elStorage , DictionaryStorage>(); servi ces . AddTransient<ProductTotalizer>() ; services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage(); app .U seStaticFiles() ; app . UseMvcWithDefaultRoute() ; В главе 14 было описано, как инфраструктура ASP. NEТ снабжает класс Startup службами, содействуя в настройке приложения; в число этих служб входит реализа­ ция интерфейса IHostingEnvironment, предназначенная для определения среды размещения. Службы можно получать как аргументы метода Configure (), но не метода ConfigureServices () , поэтому в класс Startup был добавлен конструктор. который предоставляет доступ к объекту реализации IHostingEnvironment и при­ сваивает его полю по имени env. Внутри метода ConfigureServices () с помощью метода AddTransient () оп­ ределяется фабричная функция с указанием лямбда-выражения. Лямбда-выражение получает объект System. IServiceProvider, который может использоваться для создания экземпляров других типов, зарегистрированных посредством поставщика служб. с применением методов из табл . 18.4 .
570 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 18.4 . Методы IServiceProvider и расширяющие методы Имя GetService<service>() GetRequi redServ ice<service>() Описание Этот метод использует поставщик служб для созда­ ния нового экземпляра типа службы. Он возвращает null, если для запрошенного типа отсутствует отобра жение Этот метод применяет поставщик служб для созда­ ния нового экземпляра типа службы . Он генерирует исключение, если для запрошенного типа отсутству­ ет отображение Внутри фабричной функции объект реализации IHostingEnvironment ис­ пользуется для выяснения, функционирует ли приложение в среде разрабопш. и если это так, тогда посредством метода GetServ i ce () создается экземпляр клас­ са Me moryRepo si tory, который возвращается из фабричной функции как объект, подлежащий применению для зависимости от IRep o sitory. Метод GetService () используется для создания объекта из-за того , что класс MemoryRepository имеет собственную зависимость от интерфейса IModelStorage , а применение поставщика служб означает автоматическое управление обнаружением и распознаванием зави­ симости, но это также означает необходимость указания жизненного цикла, I<оторый должен использоваться для объектов MemoryRepo si tory , например: services.AddTransient<MernoryRepository>(); Без такого оператора поставщик служб не будет располагать информацией, I<ото­ рая ему нужна для создания и управления объектами MemoryReposi t ory. Если приложение выполняется не в среде разработки. тогда фабричная функция возвращает новый экземпляр класса Al ternateReposi tory. Экземпляр этого класса может быть создан напрямую с применением ключевого слова new , потому что в его конструкторе какие-либо зависимости не объявлены. Использование жизненного цикла, ограниченного областью действия При таком жизненном цикле создается единственный объект класса реализации, который применяется для распознавания всех зависимостей, ассоциированных с од­ ной областью действия, что обычно означает одиночный НТТР-запрос . (Вы можете создавать собственные области действия, но в большинстве приложений от них мало пользы.) Поскольку стандартной областью действия является НТТР-запрос, рассматрива­ емый жизненный цикл позволяет единственному объекту разделяться всеми ком­ понентами, которые обрабатывают запрос. Чаще всего это удобно при совместном использовании общих данных контекста, когда пишутся специальные классы, такие как маршруты. Жизненный цикл, ограниченный областью действия, создается путем применения расширяющего метода AddScoped ( ) для конфигурирования поставщика служб (листинг 18.32).
Глава 18 . Внедрение зависимостей 571 Совет. Как было описано в табл. 18.4, доступны также версии метода AddScoped () , кото­ рые принимают фабричную функцию и могут использоваться для регистрации конкретно­ го типа. Такие методы работают аналогично методу AddTransient () , который демонс ­ трировался в предыдущем разделе , с вполне очевидной разницей в том, что ж изненный ци кл создаваемы х ими объектов отличается. Листинг 18.32. Использование жизн енного цикла, ограниченного областью действия, в файле Startup. cs using Microsoft.AspNetCore . Builder ; using Microsoft . Extensions . Depende ncyinjection ; using Dependencyinjection . Infrastructure ; using Dependencyinjection.Models ; using Microsoft . AspNetCore . Hosting ; namespace Dependencyinjection { puЫic class Startup { pr i vate IHostingEnvironment env ; puЫic Startup(IHostingEnvironment hostEnv) { env = hostEnv; puЬlic void ConfigureServices(IServic eCollection services) { services.AddScoped<IRepository, MemoryRepository>(); services . AddTransient< IM ode l Sto rag e , DictionaryStorage>(); services . AddTransient<ProductTotalizer>() ; services . AddMvc() ; puЬlic void Configure( IApplicationBuilder арр) { app . UseStatusCodePages() ; app .UseDeveloperExceptionPage() ; app . UseStaticFil es() ; app . UseMvcW i thDefaultRoute() ; В примере прило жения экземпляры классов HomeController и ProductTotalizer создаются вместе. чтобы обрабатывать запрос, и оба они требуют от поставщика служб распознавания зависимости от интерфейса IReposito r y . Пр именение метода AddScoped () гарантирует, что зависимости обоих объектов распо знаются посредс ­ твом единственного объекта MemoryReposi tory. Увидеть результат можно, запус ­ тив пример приложения; браузер отобразит два одинаковых идентификатора GUID (ри с . 18 . 11). П ерезагрузка страницы приводит к созданию нового НТТР- запроса, о з­ начая создание нового объекта MemoryRepos i tory. Использование жизненного цикла одиночки Жи зн енный цикл одиночки гарантирует, что для распознавания всех зависимос­ тей для заданного типа службы применяется единственный объект. При исполь зо­ вании такого жизненного цикл а вы обязаны обеспечить безопасность параллельно­ го доступа к классам р еализации, применяемым для распознавания зависимостей. В ли сти нг е 18. 33 изменена область действия для конфигурации IReposi tory.
572 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC 6dba7caa-e118-4798-b26c-aabcdeBd90C6 56912сЬО-2ес а-4ЬЗ5 -Ьбаd-5с1383426163 Bdba7caa -e 118-4798- b26c-aabcde8d90c6 Price Рис. 18.11. Эффект от жизненного цикла, ограниченного областью действия Листинг 18.33 . Использование жизненного цикла одиночки в файл е Startup. cs p u Ыic void Confi gureServices(ISe rvi c eCollection services) { services.AddSingleton<IRepository, MemoryRepository>(); se r vices . Add Tr ansient<IModel Sto r age , Di ctionaryStorage>() ; services . AddTransient<ProductT o t a l izer>() ; services . AddMvc() ; Метод AddS ingle ton () создает новый экземпляр класса Me mo r yRepositor y в первый раз , когда он должен распознавать зависимость от интерфейса I Reposi tory, и затем повторно использует этот экземпляр для всех последующих зависимостей. даже если они ассоциированы с другими НТТР - запросами (рис. 18.12). [') Dependency lnjection Х D Depende ncy ln1ection Х Tota lizer d761dae4-2104-41 dc- ·_ -~-.lE~~~::==~-:=:-:::-~1_ !_I HomeC011 troll er d761dae4-2104-41 dc-80fb-6f448de87ca9 1! Totalizer d761dae4-2 104-4 1dc-80fb -6f 448de87ca9 HomeControll er d761dae4-2104-4 1dc- i Price 1 """ .. _. ,,, .,,,. .._ ~ ,,,,.--~,. ,.,.. ~ Name Pri Name Рис. 18.12. Эффект от жизненного цикла одиночки И с пользов ан и е вн едрен ия в действия Стандартный способ объявления зависимости - через конструктор , что представ ­ ляет собой прием , который можно применять в любом классе и который полагает­ ся на средства внедрения зависимостей , являющиеся частью основно й пл атфор мы ASP. NEТ.
Глава 18 . Внедрение зависимостей 573 Инфраструк1УРа MVC дополняет стандартную функциональность альтернативным подходом, называемым внедрением в действия, который позволяет объявлять зави­ симости через парам етры методов действий. Строго говоря , внедрение в действия пре доставляется систе мой привязки мод елей , 1<0тор ая обсуждается в гл аве 26, но оно рассматривается в настоящей главе , т.к . позволя ет использовать службы по-другому . Внедрение в действия выполняется с помощью атрибута FromServices , который прим еня ется к параметру метода дей ствия, как показано в листинг е 18.34. Листинг 18.34. И спользование внедрения в действия в фа йле HomeController. cs using Microsoft . AspNetCore . Mvc; using Dependencyinjection. Mode l s ; usi ng Dependencyi njection .Infrastruc t ure ; namespace Dependencyinj e ct i on . Co n troll e rs puЫic class HomeCont rol l er : Control ler private I Repository r e pos itory; puЫic HomeControlle r ( I Repository r epo) { r epository = repo ; puЬlic ViewResult Index([FromServices]ProductTotalizer totalizer) { View Ba g. HomeControll e r = reposi t o ry. ToS t ring() ; ViewBag.Totalizer = totalizer.Repository.ToString(); return View(repos i to r y .Products) ; Инфр аструктура MVC с применением поставщика служб получает экземпляр клас­ с а ProductTotalizer и пр едоставляет его в качестве аргумента при вы з ове метод а действия Index (). Использовани е внедрения в действия мене е распространено , чем стандартно е внедрение в конструкторы, но может быть удобны м в ситуации , когда имеется зависимость от объекта, который является дорогостоящим в создании и тре ­ буется только в одном из методов де йствий, определяемых контроллером. Применение вн едр ения в конструкторы распознает зависимость для всех методов действий, даже если метод действия, вызываемый для обработки запроса, не исполь зует объект ре­ ализации. Декор и рование метода действия атрибутом FromServi ces сужает сферу влияния з ависи м ости и гарантирует, что экзе м пляр типа реализации буд ет созда­ ваться, только когда он необходим . Использование атрибутов внедре н ия в с во йства В гл аве 17 объяснялось , как получ ать данны е контекста в контролл ере РОСО за сч ет объявления сво йства и его декорирования атрибутом Control l erContext. Теперь, читая настоящую главу, вы должны понимать, что это было специальной фор­ мой внедрения зависимостей. Она называется внедрением в свойства. Инфраструктура MVC предлагает набор специализированных атрибутов, которые мо жно применять для получения сп ецифических типов через внедр ение в свойства внутри контроллер ов и компонентов представлений (рассматриваемых в главе 22). В случае наследования контроллеров от базового класса Cont r oller использовать
574 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC такие атрибуты не обязательно, потому что информация контекста доступна через удобные свойства, но в табл. 18 .5 приведен список атрибутов, предна значенных для применения в контроллерах РОСО. Таблица 18.5 . Специализированные атрибуты для внедрения в свойства Имя Contro llerContext ActionContext ViewContext ViewComponentContext ViewDataDictionary Описание Этот атрибут устанавливает свойство ControllerContext, которое предоставляет надмножество функциональности класса ActionContext, как описано в главе 31 Этот атрибут устанавливает свойство ActionContext для предоставления информации контекста методам действий . Классы Controller открывают доступ к информации кон­ текста через свойство ActionContext, а также набор удоб ­ ных свойств, описанных в главе 31 Этот атрибут устанавливает свойство ViewContext, чтобы предоставить данные контекста для операций с представ­ лениями, включая дескрипторные вспомогательные классы (глава 23) Этот атрибут устанавливает свойство ViewComponentContext для компонентов представлений, которые обсуждаются в главе 22 Этот атрибут устанавливает свойство ViewDataDict ionary, чтобы предоставить доступ к дан­ ным привязки модели, как описано в главе 26 Запрашивание объекта реализации вручную IЛавное средство внедрения зависимостей ASP.NET и дополнительные атрибуты, которые инфраструктура МVС пр едлагает для внедрения в свойства и действия, пре­ доставляют всю поддержку. требуемую в большинстве приложений для создания слабо связанных компонентов. Однако могут быть ситуации, когда удобно получать объект ре ализации для интерфейса, не пол агаясь на внедрение. В таких ситуациях можно работать напрямую с поставщиком служб, как демонстрируется в листинге 18.35. Листинг 18.35 . Использование поставщика служб напрямую в файле HomeController . cs using Microsoft . AspNetCore . Mvc ; using Dependencyinjection.Models ; using Dependencylnjection.Infrastructur e ; using Microsoft . Extensions . Dependencyinjection ; namespace Dependencyinjection . Controllers puЫic class HomeController : Cont r oller puЫic ViewResu lt Index([FromServices]ProductTotalizer totalizer) IRepository repository = HttpContext.RequestServices.GetService<IRepository>();
Глава 1В . Внедрение зависимостей 575 ViewBag . HomeController = repository.ToString() ; ViewBag . Totalizer = totalizer . Repository.ToString() ; return View(repository.Products) ; Объект HttpContext , возвращаемый свойством с таким же именем, опре­ деляет свойство Re qu е s t S е r v i се s , которое возвращает объект реализации IServiceProvider , разрешая вызов на нем методов из табл. 18.4 . В листинге 18.35 было удалено свойство Repos i tory , которое устанавливалось с применением внед­ р е ния в свойства , а взамен задействовано свойство HttpContext . RequestServices для получения реализации интерфейса IRepo si tory . Это известно как nammepн Локатор служб (Service Locator), использования ко­ торого, по мнению ряда разработчиков, следует избегать. Марк Симен сделал хо ­ рошее описание проблем, к которым он может привести (ht tp : / /Ьlog . ploeh . dk/ 201О/02 / 03/ ServiceLoca tor isanAnti - Pa ttern). Моя точка зрения отличается меньшей строгостью в том, что получение служб подобным способом совершенно обоснованно, когда обычный прием получения зависимостей через конструктор по некоторым причинам применять невозможно. Совет. Если вам нужен доступ к службам в методе Configure () класса Startup, тогда можете использовать свойство Appl ica tionServ ices, предлагаемое интерфейсом IApplicationBuilder . Резюме В этой главе объяснялась роль, которую внедрение зависимостей играет в при ­ ложении MVC. Оно помогает создавать слабо связанные компоненты, 1<оторые лег­ ко заменять и просто изолировать в целях тестирования. Было продемонстрировано средство внедрения зависимостей ASP.NET, а также ат рибуты, которые инфраструк­ тура MVC предоставляет для внедрения зависимостей в свойства и методы действий. Кроме того, были описаны варианты жизненных циЮiов , доступные при конфигури­ ровании поставщика служб , и показано, как они влияют на способ, котор ым созда­ ются объекты. В следующей главе рассматриваются фильтры, добавляющие дополни­ тельную логику в процесс обработки запросов.
ГЛАВА 19 Фильтры фильтры внедряют дополнительную логику в процесс обработки запросов МVС. Они предлагают простой и элегантный способ реализации сквозной обязаннос­ ти; таким термином обозначаете.я функциональность, которая используете.я по всему приложению и не может быть сосредоточена в одном месте, поскольку это нарушило бы принцип разделения обязанностей. Классическими примерами скво з ной обязан­ ности .явл.яютс.я регистрация в журнале, авторизация и кеширование. В настоящей главе мы рассмотрим различные категории фильтров, поддерживаемые МVС, и пока­ жем, как создавать и применять фильтры, а также каким образом управлять их выпол­ нением. В табл. 19.1 приведена сводка, позволяющая поместить фильтры в контекст. Таблица 19.1 . Помещение фильтров в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие-то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией мvс 5? Ответ Фильтры используются для применения логики к методам действий , не добавляя код в класс контроллера Фильтры позволяют использовать код, который не является частью определения действия в классическом паттерне MVC. Результатом будут более простые классы контроллеров и мно ­ гократно используемая функциональность, которая может при­ меняться повсюду в приложении Существуют разнообразные типы фильтров, которые использу­ ются инфраструктурой MVC различными способами . Наиболее распространенный способ создания фильтра предусматривает создание класса, который унаследован от атрибута, предостав ­ ляемого MVC для требуемого типа фильтра Функциональность, предлагаемая разными типами фильтров, перекрывается, поэтому выяснение, какой тип необходим, мо­ жет быть затруднено Нет, фильтры являются основным средством MVC и использу­ ются для реализации обычно требующейся функциональности, такой как авторизация Фильтры ведут себя по большому счету так же, как в предшес­ твующих версиях MVC. Существует несколько незначительных изменений. Функциональность , ранее предоставляемая фильтрами аутен­ тификации , была помещена в фильтры авторизации. Атрибут Authorize больше не является фильтром (детали применения этого атрибута приведены в главе 29) . Все типы фильтров могут определяться с использованием от­ дельны х синхронных или асинхронных интерфейсов.
Глава 19 . Фильтры 577 В табл. 19.2 приведена сводка для этой главы . Таблица 19.2. Сводка по главе Задача Внедрение дополнительной ло­ гики в обработку запросов Ограничение доступа к действиям Внедрение универсальной логи­ ки в процесс обработки запросов Инспектирование либо измене­ ние результатов, выпускаемых методами действий Обработка ошибо к Использование служб в фильтрах Помещение фильтров под управ­ ление жи зненным циклом Применение фильтров к каждому методу де й ствия в прило жении Изменение порядка, в котором применяются фильтры Решение Примените фильтры к контроллерам либо их методам действий Используйте фильтры авторизации Используйте фильтры действий Используйте фильтры результатов Используйте фильтры исключений Объявите зависимости в конструкторе фильтра, зарегистрируйте службу в классе Startup и примените фильтр с помощью атрибута TypeFil ter Используйте жизненные циклы внед­ рения зависимостей для регистрации фильтров в классе Startup и приме­ ните фильтры с использованием атри­ бута ServiceFilter Используйте глобальный фильтр Используйте параметр Order Подготовка проекта для примера Листинг 19.1 -19.11 19.12, 19.13 19.14 -19.16 19.17-19.21 19 .22, 19.23 19.24 -19.28 19.29-19.31 19.32-19.34 19.35-19.37 В этой главе при создании примера приложения будет применяться тот же самый подход, что и в предшествующих главах. Создайте новый проект типа Empty (Пустой) по имени Filters с использованием шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP. NEТ Core (.NЕТ Core)). Добавьте требуемые пакеты NuG et в раз­ дел dependencies файла proj ect . j son и настройте инструментарий Razor в разде ­ ле tools, как показано в листинге 19.1. Листинг 19.1. Добав ление пакетов в файле project. j son "dependencies ": { " Microsoft.NETCore.App" : "version": "1.0.О", "type": "platforrn" }'
578 Часть 11. Подробные сведения об инфрастру ктуре ASP.NET Core MVC )' " Microsoft .AspNetCore . Diagnost i cs ": "1 . О . О ", " Microsoft . AspNetCore . Server . IISintegration ": " 1 . 0 . 0 ", " Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ", " Microsoft . Extensions . Logging . Console ": " 1 . 0 . О ", "Microsoft . AspNetCore . Mvc" : "1. О. О" , "Microsoft . AspNetCore . StaticFiles": "1.0.0", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0 . 0-preview2-final", "type": "Ьuild" " tools ": }' "Microsoft . AspNetCore.Razor.Tools": "1.0.0 -preview2-final", " Microsoft.AspNetCore . Server .I ISintegration.Tools ": "1 .0 .0-preview2-final" " frameworks 11 : { 11 netcoreappl . O": " imports " : ["dotnet5. 6 11 , 11 portaЬle - net 4 5+win8 11 ] )1 " buildOptions ": 11 emitEntryPoint 11 : true , " preserveCompilationContext 11 : true )' " runtimeOptions" : { 11 configProperties ": { 11 System . GC . Server ": true) В листинге 19.2 приведен класс Startup, который конфигурирует средства , пре­ доставля ем ы е пак етами NuGet. Листинг 19.2. Содержимое файла Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions .D ependencyinjection ; namespace Fil ters { puЫic class Startup puЫic void ConfigureServices(IServiceCollect ion services) { services.AddМvc(); puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute();
Глава 19 . Фильтры 579 Включение SSL Для ряда примеров в настоящей главе требуется применение протокола SSL, ко­ торый по умолчанию отключен. Чтобы включить SSL, выберите пункт Filter Properties (Свойства фильтров) в меню Project (Проект) среды Visual Studio и отметьте флажок ЕnаЫе SSL (Включить SSL) на вкладке Debug (Отладка), как показано на рис. 19. l . Примите к сведению назначенный номер порта, который в каждом проекте будет отличаться. Арр URL: '1ttp:/Лocгlhos t51000/ ~ Enable SSL https:!Лocalhost44З18f ~ ЕnаЫе Anonymous Authen tica tion О Enabk \л/indows Authenьcation Рис . 19. 1. Включение SSL Создание контроллера и представления -- '"] Контроллеры в этой главе будут простыми, т.к. внимание сосредоточено на по­ мещении логики в другое место внутри приложения. Создайте папку Controllers, добавьте в нее файл класса по имени HomeController. cs и определите в нем конт­ роллер, как продемонстрировано в листинге 19.3. Листинг 19.З. Содержимое файла HomeController.cs из папки Controllers using Microsoft . AspNetCore . Mvc; namespace Filters . Controllers { puЫic c lass HomeController : Controller puЫic ViewResul t Index () => View ( "Mess age ", "This is the Index action on the Home controller"); Метод действия Index () визуализирует представление по имени Message, пере­ давая ему в качестве данных представления строку. Создайте папку Views/Shared и поместите в нее файл представления Razor по имени Message . cshtml с разметкой из листинга 19.4. Листинг 19.4. Содержимое файла Message. cshtml из папки Views/Shared @{ Layout = null; < ! DOCTYPE html> <html> <head> <meta name="viewport" content="width=device - width" /> <titl e>Filters</title> <link asp-href-include="liЬ/bootstrap/dist/css/* .min.css" rel="stylesheet" /> </head>
580 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC <body class= "p anel - body " > @if (Model is string) { @Mod el else if (Model is IDictionary<string, string>) { var dict = Model as IDictionary<string , string> ; <tаЫе c l ass= " ta Ы e taЫe - condensed taЫe - striped taЫe-bordered " > <thead><tr><th>Name</th><th>Value</th ></tr></thead> <tbody> @foreach (var kvp in dict) { <tr><td>@kvp.Key</td><td>@kvp.Value</td></tr> </tbody> </tаЫе> </body> </html> Представление является слабо типизированным и отображает либо значение string, либо объект Dict i onary<st ring , string> ; во втором случае выводится таблица. При стилизации НТМL-элементов представление полагается на пакет CSS из Bootstrap. Для добавления Bootstrap в проект создайте файл bower . j son с использо­ ванием шаблона элемента Bower Configuration File (Ф айл конфигурации Bower) и до­ бавьте пакет Bootstrap в раздел dependenc i es (листинг 19.5) . Листинг 19.5 . Добавление пакета Bootstrap в файле bower. j son "name": "asp.net", " private" : true, " dependencies ": { "bootstrap": 11 3.3.6 11 Последний подготовительный шаг предУсматривает создание в папке Views файла _ Viewimports . cshtml, который устанавливает встроенные дескрипторные вспомо­ гательные классы для применения в пр едставлениях Razor (листинг 19.6). Листинг 19.6 . Содержимое файла _Viewimports. cshtml из папки Views @a ddT agHelp er * , Mic r osoft.AspNetCore . Mvc . TagHelpers Запустив приложение, вы увидите вывод, показанный на рис. 19.2 . This is the lndex action 011 the Ноте controller -------------·-------------~--·-.J Рис. 19.2 . Запуск примера приложения
Глава 19. Фильтры 581 Использование фильтров Фильтры позволяют изъять логику, которая иначе применялась бы в методе дейс­ твия, из контроллера и определить ее в многократно используемом классе. В качестве примера предположим, что нужно обеспечить возможность доступа к методам дейс­ твий только по НТГРS, а не по обычному незашифрованному протоколу НТГР. Объект контекста HttpRequest предоставляет информацию, необходимую для выяснения, применяется ли НТГРS (листинг 19.7). Листинг 19.7. Проверка на предмет использования НТТРS в файле HomeController. cs using Microsoft . AspN etCore.Mvc; using Microsoft.AspNetCore.Http; namespac e Filters . Controllers { puЫic class HomeController : Controller puЬlic IActionResult Index() { if ( !Request. IsHttps) { return new StatusCodeResult(StatusCodes.Status403Forbidden); }else{ return V:i.ew ( "Message", "This is the Index action on the Ноте controller"); Именно так решалась бы задача с НТГРS без фильтров. После запуска приложения браузер будет запрашивать стандартный URLдля проекта, отличный от НТГРS, который метод действия Index () обработает путем возвращения объекта StatusCodeResul t , отправляющего код состояния НТГР 403 в ответе (как описано в главе 17). Если за­ просить стандартный URL для НТГРS , например, https: //localhost : 44318 , тогда метод действия I ndex () отреагирует визуализацией представления Message (пр ежде чем браузер отобра зит результат, может понадобиться подтвержд ение в окне предуп­ реждения безопасности) . Оба исхода иллюстрируются на рис. 19.3 . Совет. Очистите хронологию в браузере, если вы не получаете ожидаемые результаты при выполнении примеров из данного раздела. Браузеры часто отклоняют отправку серверу запросов, которые сгенерировали ошибки SSL, что является хорошей практикой защиты, но может мешать во время разработки. [j lcx:alho st: 51000 Х 1 С [(D-locall~~-st-:5-100......:::0= 1 Status Cod~~-~ 3; - Fo 1·Ьi~~:-1 ..: . . _ __ _ ? г~~!!~~~t~44318 -·--~-=~ 1 L ___ ____ ____ j This is the lndex action on the Home controlfer 1 i L--------· -- - - - - Рис. 19.З. Ограничение доступа только запросами HTTPS
582 Часть 11 . Подробные сведения об инфраструктуре ASP.NEТ Соге MVC Код в листинге 19.7 работает. но в нем присутствуют проблемы . Первая пробле­ ма заилючается в том, что метод действия содержит код, который имеет отношение больше к реализации политики безопасности, чем к обработ1<е запроса, обновлению модели и выбору ответа. Более серьезная проблема связана с тем , что решение с ко­ дом обнаружения НТТР внутри метода действия плохо масштабируется и этот код должен быть продублирован в каждом методе действия, определяемом в контролл е ре (листинг 19.8). Листинг 19.8. Добавление метода действия в файле HomeCon trol ler. cs using Microsoft.AspNet Core.Http; usi ng Microsoft.AspNet Core.Mvc; namespace Fil ters. Contr ol lers { puЫic class HomeCont rol ler : Control ler puЫic IActionRe s ul t Index () { if ( !Request . IsHttps) { r etur n new Stat us CodeResult( S ta t usCodes . Stat u s403Forbidden); else{ r eturn View("Message", "Thisis theIndex actionontheНоте controller"); puЫic IActionResult SecondAction() { if ( !Request. IsHttps) { return new StatusCodeResult(StatusCodes.Status403Forbidden); else { return View("Message", "This is the SecondAction action on the Home controller"); Нужно помнить о реализации той же самой проверки внутри каждого метода дейс­ твия в каждом контроллере, для которого планируется требование протокола HTTPS. Код реализации политики безопасности занимает значимую часть довольно простого контроллера и затрудняет его понимание. Кроме того , забыть добавить его в н овый метод действия и тем самым создать брешь в политике безопасности - дело только времени. Проблемы такого рода способны решать фильтры, как демонстрируется в листинге 19.9 . Листинг 19.9. Применение фильтра в файле HomeController. cs using Mi crosoft.AspNet Core . Http; using Microsoft.AspNetCor e .Mvc; namespace Filters . Contro l lers { puЫic cl ass HomeController : Controller [RequireHttps] puЬlic ViewResul t Index () => View ( "Message" , "This is the Index action on the Home controller");
Глава 19. Фильтры 583 [Requireнttps] puЫic ViewResul t SecondAction () => View ( "Message", "This is the SecondAction action on the ноте controller"); Атрибут Re q uireHtt p s применяет один из встроенных фильтров J\ классу HomeContro l l er. Он ограничивает доступ к метолам действий, так что поддержива­ ются только запр о сы НТТРS, и позволяет удалить из всех м етодов код, относящийся к безопасности, и сосредоточиться на обработке усп ешных запросов. На заметку! Фильтр Requi re Https работает не совсем так, как специальный код из листин­ га 19.7 . Для запросов GET атрибут Requi r eHttp s обеспечивает перенаправление кли­ ента на первоначально запрошенный URL, но делает это с использованием схемы h t t p s , в результате чего запрос h t tp : / / l ocalho st/Ho me/ Index будет перенаправлен на https: / / localhost/Home/ Inde x . Такой подход имеет смысл в большинстве развер­ нутых приложений, но не на стадии разработки, поскольку протоколы НТТР и НТТРS обслу­ живаются на разных локальных портах. В классе Requ i r eH ttp sAt t r i bute определен защищенный метод по имени HandleNonH ttps Re q ue s t (), который можно переопре­ делить для изменения поведения. В качестве альтернативы в разделе "Использование фильтров авторизации" исходная функциональность воссоздается с нуля. Разумеется, по-прежнему необходимо помнить о применении атрибута RequireH t tps к каждому методу действия, о чем впол не можно забыть. Но филь­ тры поддерживают полезный трюк: применение атрибута к классу контроллера дает такой же эффект, как и его применение ко всем индивидуальным методам действий (листинг 19. 10). Листинг 19.10. Применение фильтра ко всем методам действий в файле HomeController. cs using Microsoft . AspNetCore .H ttp ; using Mi crosoft . AspNetCore . Mv c; namespace Filters . Cont r olle rs { [RequireHttps] puЫic c l ass HomeContro l ler : Contro l ler puЫic ViewResult I ndex () => View("Me ssage", " This i s the Index action on the Home contr oller" ); puЫic ViewResul t SecondActi on() => View("Message", " Thi s is the SecondAction acti on оп t he Home control ler" ) ; Фильтры могут применяться с разными уровнями детализации. Если вы хотите ограничить доступ к одним действиям, но не ограничивать его к другим, тогда приме­ няйте атрибут RequireHttps только к нужным методам. Чтобы защитить все методы действий, в том числе любые методы, которые будут добавл ены в будущем, приме­ няйте атрибут RequireHt t p s к классу . Если же вы желаете применить какой-нибудь фильтр к любому действию в приложении, то можете использовать глобалы-tые фш~ь­ тры. кот орые будут описаны далее в глав е.
584 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC П онятие фильтров Теперь, когда вы видели, как используются фильтры , наступило время объяснить . что происходит "за кулисами". Фильтры реализуют интерфейс IFi l terMetad ata , ко­ торый находится в пространстве имен Mi c r o s oft . AspNetCore . Mvc . Fil ters. Вот его определение : names p ace Microsoft . Asp NetCore . Mvc . Filters pu Ыi c interface IFilterMetadata { } Интерф ейс пуст и не требует от класса фильтра реализации какого-либо специфи ­ ческого поведения. Причина в том, что существует несколько отдельных типов филь­ тров, каждый из которых работает по-разному и служит для своих целей. В табл. 19. 3 описаны типы фильтров , определяющие их инт е рфейсы и то, что они делают. (Инфраструктура MVC поддерживает и другие типы фильтров. но они не используются напрямую . Взамен они интегрированы в ср едства, рассматрива е мы е в других главах, и применяются ч е рез специфические атрибуты, включая атрибуты Produ ces и Cons um es , 1юторы е обсуждаются в главе 20.) Таблица 19.З. Различные типы фильтров Разновидность Фильтры авторизации Фильтры действий Фильтры результатов Фильтры исключений Интерфейсы IAuthorizationFilter I AsyncAu thori zationFilter IActionFilter IAsyncActionFilter I Res u ltFilter IAsyncResultFilter IExcepti onFil ter I AsyncExcept ionFi l t er Описание Этот тип фильтров используется для применения политики безопасности прило жения , включая авторизацию пользователей Этот тип фильтров используется для выполнения работы немедленно перед или после выполнения метода действия Этот тип фильтров используется для выполнения работы немедленно перед или после обработки результата , полу­ ченного из метода действия Этот тип фильтров используется для обработки исключений В табл. 19.3 приведены неч еткие описания, потому что фильтры можно исполь­ зовать для широкого спектра задач. ограниченного только вашим воображением и проблемами, которые необходимо решить. По мере погружения в детали их работы все станет яснее , а пока следует уяснить два важных момента . Во-первых, для каждого типа фильтра в табл. 19.3 предусмотрены два разных ин­ терфейса . Фильтры могут выполнять свою работу синхронно или асинхронно, так что синхронный фильтр результатов, например, реализует интерфейс IResu l tFilter, тогда как асинхронный - интерфейс IAsync Re s u ltFilter. Во-вторых, фильтры прим еняются в особом порядке. Фильтры авторизации вы­ полняются первыми, з а ними идут фил ьтры действий, а з ат ем фильтры результ атов. Фильтры исключений выполняются только в случае генер ации какого-либо исключе­ ния , которо е наруша ет нормальную последовательность .
Глава 19. Фильтры 585 Получение данных контекста Фильтры обеспечиваются данными контекста в форме объекта Fil terContext. Класс Fil terContext является производным от ActionContext, который также представляет собой базовый класс для юшсса ControllerContext, рассмотренного в главе 17. Из соображений удобства в табл. 19.4 перечислены свойства, унаследо­ ванные от класса ActionContext , наряду с дополнительными свойствами, которые определены в FilterContext. Таблица 19.4 . Свойства FilterContext Имя ActionDescriptor HttpContext ModelState RouteData Filters Описание Это свойство возвращает объект ActionDescriptor, который описывает метод действия Это свойство возвращает объект HttpContext, который предо­ ставляет детали НТТР-запроса и отправляемого взамен НТТР-ответа Это свойство возвращает объект ModelStateDictionary, кото­ рый используется для проверки достоверности данных, отправлен­ ных клиентом (глава 27) Это свойство возвращает объект RouteData, который описывает способ обработки запроса системой маршрутизации (глава 15) Это свойство возвращает список фильтров, которые были применены к методу действия, выраженный как IList<IFilterMetadata> Использование фильтров авторизации Фильтры авт оризации применяются для реализации политики безопаснос­ ти приложения. Фильтры авторизации выполняются перед фильтрами других ти­ пов и перед запуском метода действия. Вот как выглядит определение интерфейса IAuthorizationFilter: namespace Microsoft . AspNetCore . Mvc.Filters { puЫic interface IAuthorizationFilter : IFilterMetadata void OnAuthorizat ion(Authori zat ionFilterContext context ) ; Метод OnAuthorization () вызывается для предоставления фильтру возможнос­ ти авторизовать запрос. Вот определение интерфейса IAsyncAuthorizationFilter, используемого асинхронными фильтрами авторизации: using System.Threading.Tasks; namespace Microsoft.AspNetCore . Mvc.Filters puЫic interface IAsyncAuthorizationFi l ter : IFilterMetadata { Task OnA uthorizatio nAsync(AuthorizationFilterContext context);
586 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC Метод OnAuthorizationAsync () вызыв ается, чтобы фильтр мог авторизов ать за­ прос. КШ{ОЙ бы интерф е йс ни пр име нялся, фильтр получает данны е конте кста , опи­ сывающие запрос , чер ез экземпляр класса Aut h orizationFilterContext , которы й является производны м от класса Filter Context и добавляет одно в аж ное свойство, приведенное в табл . 19.5 . Таблица 19.5. Свойство AuthorizationFilterContext Имя Result Описание Это свойство I ActionResul t устанавливается фильтрами авторизации, когда запрос не удовлетворяет политике авторизации прило жения . Если оно установлено, тогда вместо вызова метода действия инфраструктура MVC ви­ зуализирует объект реализации IActi onResul t Создание фильтра а вторизации Чтобы посмотр еть , как работают фильтры авторизации , со здайт е в проекте папку Infrastruct u re , поместите в нее файл класса по имени HttpsOnlyAttribute . cs и определите в нем фильтр, как показано в листинге 19. l l . Листинг 19.11. Содержимое файла HttpsOnlyAttribute. cs и з папк и Infras tructure using System ; us i ng Microsoft . AspNetCo r e . Http ; using Micro s oft .AspNetCo r e. Mvc ; using Microsoft . AspNetCore . Mvc . Filters ; namespace Filters . Infrastructure { puЬl i c class HttpsOnl yAtt r ibute : Attribute , IAuthorizationFilter puЫic void OnAut h orization(Auth oriza t ionFi lterContex t context) i f ( ! context.HttpContext . Request . IsHttp s ) { context. Result = new StatusCodeResult(StatusCodes . Status403Forbidden); Если запрос удовлетворяет политике авторизации , то фил ьтр автор иза ции нич ег о не делает; такая пассивность позволяет инфраструктур е MVC перейти к следующему фильтру и в конечном итоге выполнить метод действия. На заметку! Атрибут Authorize , который может использоваться для ограничения доступа специфическими пользователями и группами, был реализован в виде фильтра , но в ASP.NET Core MVC это больше не так. Атрибут Authori ze по-прежнему применяется, но работа­ ет по-другому. Внутренне имеется глобальный фильтр (глобальные фильтры обсуждаются позже в главе) , который обнаруживает атрибут Au t horize и обеспечивает соблюдение политик, определенны х системой ASP.NET Саге ldeпtity, однако атрибут Authorize не яв­ ляется фильтром и не реализует интерфейс IAuthorizationFil ter . Использование системы ASP.NET Core ldeпtity и атрибута Au thori ze рассматривается в главе 29 .
Глава 19 . Фильтры 587 При нал ичи и пробл ем ы фильтр устанавливает свойство Resu l t объекта AuthorizationFi l terContext, который передается методу OnAuthor i zat i on () . Это препятствует дальне йшему выполнению и предоставля ет инфраструктур е MVC р езульт ат для возвращения кли е нту. В листинге 19 .11 класс HttpsOn l yAt t rib u te инспектирует свойство I sHttps объекта контекста Htt p Request и устанавливает свойство Resu l t, чтобы пр е рвать выполнение, если запрос был сделан без НТГРS . В листинг е 19 . 12 новый фильтр приме ня ется к контроллеру Home . Листинг 19.12. Применение специального фильтра в файле HomeCon troller. cs using Microsoft .As p NetCore .Mvc ; using Filters.Infrastructure; namespace Filters . Cont r ol lers { [HttpsOnly ] puЫic class HomeControll er : Control l er puЫic ViewRes ul t Index () => Vi ew ( " Message ", " This is the Index action on the Home controller " ) ; puЫic ViewResult SecondAc tion () => Vi ew ( " Message ", " This i s the SecondAction action on the Home cont roller " ) ; Этот фильтр воссоздает функциональность , которая был а вкл юч ена в методы действий из листинга 19.8. Такой прием менее полезен в реальных проектах, чем вы ­ полнени е перен аправления подобно встроенному фильтру Requ ireHttp s, поскольку пользователи могут не понять смысл кода состояния 403, но демонстрирует хороший пример того . как работают фильтры авторизации. Модульное тестирование фильтров Большая часть работы при модульном тестировании фильтра связана с настройкой объекта контекста, который передается методам фильтра. Объем требующейся имитации зависит от информации контекста, используемой фильтром. В качестве примера ниже приведен мо­ дульный тест для фильтра HttpsOnly из листинга 19.11 . using System . Linq ; using Filters .I n fr astruct u re ; using Microsoft . As pNetCore . Http; using Microsoft . AspNetCo r e . Mvc ; using Microsoft . AspNetCore . Mvc . Abst ra ction s ; using Microsoft . Asp NetCo r e .Mvc . Filte r s ; usingMoq; using Xunit ; namespace Tests puЬlic class FilterTests [Fact] puЫ i c void TestHttpsFi lter() 11 Организация var httpRequest = new Mock<HttpRequest>() ; httpRequest . SetupSequence (m => m. Is Ht tps ) .Returns(true ) . Returns(false) ;
588 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC var httpContext = new Mock<HttpContext>() ; httpContext.SetupGet(m => m.Request) .Returns(httpRequest.Object); var actionContext = new ActionContext(httpContext.Object, new Microsoft . AspNetCore . Routing . RouteData() , new ActionDescriptor()) ; var authContext = new AuthorizationFilterContext(actionContext , EnumeraЫe.Empty<IFilterMetadata>() .ToList()) ; HttpsOnlyAttribute filter = new HttpsOnlyAttribute(); 11 Действие и утверждение filter.OnAuthorization(authContext); Assert . Null(authContext . Result) ; filter . OnAuthorization(authContext) ; Assert . IsType(typeof(StatusCodeResult) , authContext . Result) ; Assert . Equal(StatusCodes.Status403Forbidden , (authContext.Result as StatusCodeResult) .StatusCode) ; Тест начинается с создания имитированных объектов контекста Н t tpReque s t и HttpContext , которые позволяют представить запрос с или без НТТРS . Необходимо про­ тестировать оба условия, что делается следующим образом: httpRequest .SetupSequence (m => m. IsHttps) .Returns (true) . Returns (false) ; Этот оператор настраивает свойство HttpRequest. IsHttps таким образом, что оно возвращает последовательность значений: свойство возвращает true, когда читает­ ся в первый раз, и false - во второй раз. Имеющийся объект HttpContext можно применить для создания объекта ActionContext, который позволит создать объект AuthorizationContext , необходимый модульным тестам. За счет инспектирования свойства Resul t объекта Au thori za tionFil terCon text выполняется проверка, как фильтр реагирует на запросы, отличные от HTTPS, и затем проверка того , что происходит с запросами НТТР. Для настройки объекта AuthorizationFilterContext требуется много типов, которые полагаются на многочисленные пространства имен ASP.NET Соге и MVC, но после получения объекта контекста написание оставшейся части теста будет от­ носительно простым. Использование фильтров действий Понять фильтры действий проще всего, взглянув на интерфейс, который их опре­ деляет. Вот юш выглядит интерфейс IActionFilter: namespace Microsoft.AspNetCore.Mvc . Filters puЬlic int erface IActi on Filter : IFilterMetadata void OnActionExecuting(ActionExecutingContext context) ; void OnActionExecuted(ActionExecutedContext context) ;
Глава 19. Фильтры 589 Когда к методу действия применен фильтр действий, метод OnActionExecut ing () вызывается прямо перед вызовом метода действия, а метод OnActionExecuted () - сразу после вызова метода действия. Фильтры действий снабжаются данны ­ ми 1<онтекста чер ез два класса контекста: ActionExecutingContext для метода OnAc tionE xecuting () и ActionExecutedContext для метода OnActionExecuted ( ) . Оба класса контекста расширяют класс Fil terContext, свойства которого бьmи пе­ речислены в табл. 19.4. В классе ActionExecutingContext, используемом для описания действия, кото­ рое планируется вызвать, определены дополнительные свойства, представленные в табл . 19.6. Таблица 19.6 . Свойства ActionExecutingContext Имя Controller ActionArguments Result Описание Это свойство возвращает объект контроллера, методы действий которого должны быть вызваны. (Детали метода действия до­ ступны через свойство ActionDescriptor, унаследованное от базовых классов.) Это свойство возвращает индексированный по имени словарь аргументов, которые будут переданы методу действия. Фильтр может вставлять, удалять либо изменять аргументы Если фильтр присваивает этому свойству объект реализации IActi onRes ult , тогда произойдет обход процесса обработ ки запросов, и результат действия будет применяться для генера­ ции ответа клиенту без обращения к методу действия Класс ActionExecutedContext используется для представления действия, кото­ рое было выполнено, и определяет свойства, описанные в табл. 19.7 . Таблица 19. 7 . Свойства ActionExecutedContext Имя Controller Canceled Exception ExceptionDispatchinfo ExceptionHandled Result Описание Это свойство возвращает объект Controller, чей метод действия будет вызван Это свойство типа bool устанавливается в true, если дру­ гой фильтр действий предпринял обход процесса обработки запросов за счет присваивания результата действия свойству Result объекта ActionExecutingContext Это свойство содержит любой объект Exception, который был сгенерирован методом действия Это свойство возвращает объект ExceptionDispatchinfo, который содержит информацию трассировки стека для любо­ го исключения, сгенерированного методом действия Установка этого свойства в true указывает, что фильтр обрабо­ тал исключение, которое дальше распространяться не будет Это свойство возвращает объект реализации IActionResul t , возвращенный методом действия. При необходимости фильтр может изменить или полностью заменить результат действия
590 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Создание фильтра действий Фильтры действий являются универсальным инструментом и могут применяться для реализации любой сквозной обязанности в приложении. Фильтры действий можно использовать для прерывания обработки запроса перед вызовом действия и для изме­ нения результата после выполнения действия. Простейший способ создания фильтра действий предусматривает наследование класса от класса ActionFil terAttribute, который реализует интерфейс IActionFilter. Добавьте в папку Infrastructure файл класса по имени ProfileAttribute. cs и определите в нем фильтр, как проде­ монстрировано в листинге 19.13. Листинг 19.1 З. Содержимое файла ProfileAttribute. cs из папки Infrastructure using System . Diagnostics; using System.Text; using Microsoft.AspNetCore.Mvc.Filters; namespace Filters.Infrastructure puЫic class ProfileAttribute : ActionFilterAttribute private Stopwatch timer; puЫic override void OnAct ionExe cuting(ActionExecutingContext context) { timer = Stopwatch.StartNew(); puЫic override void OnActionExecuted(ActionExecutedContext context) { timer .Stop () ; string result = " <div>Elapsed time: " + $"{timer.Elapsed . Tota1Milliseconds} ms </div>"; byte[J byte s = Encoding.ASCII.GetBytes(result); context.HttpContext . Response . Body . Write(bytes , О , bytes . Length) ; В листинге 19 .13 объект Stopwa tch применяется для измерения количества миллисекунд. в течение которых выполнялся метод действия, запуская таймер в методе OnActionExecuting () и останавливая его в методе OnActionExecuted (). Чтобы пометить результат, с использованием объекта контекста получается ответ HttpResponse, в который включается простой фрагмент НТМL-р азметки . В листинге 19.14 иллюстрируется применение атрибута Profile к контроллеру Н оте . (Кроме того, предыдущий фильтр удален, так что будут приниматься запросы по стандартному протоколу НТТР.) Совет. Как ни странно, контроллеры являются также и фильтрами действий. Базовый класс Controller реализует интерфейсы IActionFil ter и IAsyncActionFil ter , а это значит, что можно переопределить методы, определяемые упомянутыми интерфейсами, и создать функциональность фильтра действий. Для контроллеров РОСО инфраструктура MVC инспектирует классы и проверяет, реализуют ли они любой из интерфейсов фильтра действий, и если это так, то автоматически использует их в качестве фильтров действий .
Глава 19. Фильтры 591 Листинг 19. 14 . Применение фильтра в файле HomeController . cs using Microsoft . AspNetCore . Mvc; using Filters.Infrastructure; namespace Filters.Controllers { [Profile] puЫic class HomeController : Controller puЬlic ViewResul t Index () => View ( "Message", "This is the Index action on the Home controller"); puЫic ViewResul t SecondAction () => View ( " Message", " This is the SecondAction action on the Home controller " ) ; Запустив приложение, вы увидите сообщение, подобное показанному на рис. 19.4 . Количество миллисекунд будет варьироваться в зависимости от скорости машины разработки. На заметку! При записи фрагментов НТМL-разметки прямо в ответ производится расчет на то­ лерантность браузера к неправильно оформленным НТМL-документам: элемент di v, гене­ рируемый в фильтре , находится в начале тела ответа перед элементами DOCTYPE и html, которые указывают на начало НТМL-документа , выпускаемого представлением Razor. Такой прием работает и может быть удобным для отображения диагностической информации, но на него не следует полагаться при реализации производственных функций . 1 Elapsed tiшe: 0.3354 шs Рис. 19.4. Использование фильтра действий Создание асинхронного фильтра действий Интерфейс IAsyncActionFilter применяется для определения фильтров действий, которые оперируют асинхронно. Ниже показано определение этого интерфейса: using System . Threading . Tasks; namespace Microsoft.AspNetCore . Mvc . Filters puЫic interface IAsyncActionFilter : IFilterMetadata Task OnActionExecutionAsync(ActionExecutingContext context , ActionExecutionDelegate next);
592 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC В интерфейсе имеется единственный метод, который посредством продолжения задачи позволяет фильтру запускаться до и после выполнения метода действия. В лис­ тинге 19 .15 используется метод OnActionExecutionAsync () из фильтра Profile. Листинг 19.15. Создание асинхронного фильтра действий в файле ProfileAttriЬute. cs using System . Diagnostics ; usin g System.Text; using System.Threading .T asks ; usin g Microsoft .AspN etCore .Mv c . Filters; namesp ace Filters.Infrastructure puЬlic class ProfileAttribute : ActionFilterAttribute puЫic override async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { Stopwatch timer = Stopwatch.StartNew(); await next () ; timer.Stop(); string result = "<div>Elapsed time: " + $ 11 {timer.Elapsed.TotalMilliseconds} ms</div>"; Ьуtе[] Ьytes = Encoding.ASCII.GetBytes(result); await context.HttpContext .Response.Body.WriteAsync(bytes, О, bytes. Length); Объект ActionExecutingContext снабжает фильтр данными контекста, а объект ActionExectionDelegate представляет метод действия (или следующий фильтр), подлежащий выполнению. Фильтр выполняет свою подготовительную работу перед вызовом делегата и затем завершает работу, когда делегат заканчивает функциони­ рование. Делегат возвращает объект Task, поэтому в листинге применяется ключевое слово awai t. Использование фильтров результатов Фильтры результатов применяются до и после того, как инфраструктура MVC об­ работала результат, возвращаемый методом действия. Фильтры результатов способ­ ны изменять или заменять результат действия либо полностью аннулировать запрос (даже если метод действия уже был вызван). Вот интерфейс IResultFilter, который определяет фильтр результатов: namespace Microsoft.AspNetCore.Mvc.Filters puЬlic interface IResultFilter : IFilterMetadata void OnResultExecuting(ResultExecutingContext context); void OnResultExecuted(ResultExecutedContext context);
Глава 19 . Фильтры 593 Фильтры ре зультатов следуют то му же самому шаблону , что и фильтры действий . Метод OnResultExecut ing () вызывается до того, кан результат действия, выпущен­ ный методом действия, будет обработан, и снабжается информацией контекста через объект Resul tExecutingContext. Класс Res ul t ExecutingCon text является про­ изводным от класса FilterContext и определяет дополнительные свойства , описан­ ные в табл. 19.8. Таблица 19 .8 . Свойства Resul tExecutingContext Имя Controller Cancel Result Описание Это свойство возвращает объект контроллера, чей метод действия был выполнен Установка этого свойства типа bool в true остановит обра­ ботку результата действия для генерации ответа Это свойство возвращает объект реализации I ActionResu l t, возвращаемый методом действия Метод OnResu l tExecuted () вызывается после обработки инфраструктурой MVC результ ата действия и снабжается данными контекста через экземпляр класса Resul tExecutedContext, который в дополнение к свойствам, унаследо в анным от Fi lterContext, определяет свойства, перечисленные в табл . 19 .9 . Таблица 19.9. Свойства Resul tExecutedContext Имя Controller Canceled Excepti on Описание Это свойство возвращает объект контроллера, чей метод дейс­ твия был выполнен Это свойство типа bool указывает, был ли аннулирован запрос Это свойство содержит любой объект Excepti on, который был сгенерирован методом действия Except ionDispatchinfo Это свойство возвращает объект ExceptionDispatchinfo, который содержит информацию трассировки стека для любого исключения, сгенерированного методом действия ExceptionHandled Result Установка этого свойства в true указывает, что фильтр обработал исключение, которое дальше распространяться не будет Это свойство возвращает объект реализации IActionResult, который использовался для генерации ответа клиенту Создание фильтра результатов Класс Re s и1t Fi1t еr Аt t r ibut е реализует интерфейсы фильтров результатов и предлагает самый легкий способ создания фильтра результатов , который может при­ меняться как атрибут. Чтобы посмотреть на фильтр результатов в работе , добавьте в папку Infrastructure файл класса по имени ViewRes ul t Deta il sAttribu te . c s с опред елени е м фильтра и з листинга 19.16.
594 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 19. 16. Содержимое файла ViewResul tDetailsAttribute. cs из папки Infrastructure usi ng System. Collections . Generic; u si ng Microsoft . AspNetCore . Mvc ; u s ing Microsoft . AspNetCore . Mvc . Filter s; usi n g Microsoft . AspNetCore . Mvc.ModelBinding ; using Microsoft.AspNetCore .Mvc . ViewFeatur es; namespace Filters . Infrastructur e { p u Ьl ic class Vi ewR e s u ltDetai lsAttribute : ResultFilterAttribute { p uЫic override void OnRe s ultExec u ting(Resul t ExecutingContext context) Dicti on ary<string, string> dict = n ew Dictionary <string , s tri ng> { ["Resul t Туре"] = context.Res ult.GetType() .Name }; ViewResult vr ; if ((vr = context.Result as ViewResult) != null) { dict["View Name"] = vr.ViewName; dict["Model Туре"] vr.ViewData.Model .GetType() .Name; dict["Model Data"] = vr .ViewData.Model.ToString(); context. Result = new Vi e wRes ul t { ViewName = " Message ", }; Vi ewData = new View Da t aDictiona r y( new EmptyMod e l Metad ataProvi d e r() , new ModelStateDictionary () ) { Mode l d i ct } Класс Vi e wRes ul tDetai ls At tri bu t e переопределяет единств е нный м ето д OnResultExecuting () и исполь зует объект контекста для и з мен е ния результа­ та действия, применяемого при генерации ответа 1шиенту. Фильтр создает объект ViewResul t, который визуализируе т представление Me s sage , используя словарь с простой диагностической информацией в качестве модели представления. Метод OnResul tExecuting () вызывается после того, как метод действия вы­ пустил результат действия , но перед его обработкой с целью генерации результата. Изменение значения свойства Re s ul t объекта контекста позволяет предо ставлять другой тип резул ьтата из метода действия, к которому фильтр был прим енен . В лис ­ тинге 19. 17 фильтр результатов применяется к контроллеру Home. Листинг 19.17. Применение фильтра результатов в файле HomeController.cs us i ng Microsoft . AspNe tCore . Mvc ; usi ng Filters . Infrast ructur e ; namespace Fil t ers . Controlle rs { [ViewResultDetails] puЫi c class HomeContro l ler : Controller puЫic Vi ewResul t I ndex() => View( "Me s sage", "This is the Index action on the Home controller"); puЬlic ViewResul t SecondAct i on () = > View ( " Message ", " Thi s is the SecondActi on acti on on the Home cont roller" ) ;
Глава 19 . Фильтры 595 Запустив приложение, вы увидите эффект от фильтра результатов (рис. 19.5). Name Value Result Туре View Name ViewResult Messa.ge Model Туре String L M~-d._e__' _o_a_ta--This is the lnd-~ action-on th~:. ome -~ontroll:____J Рис. 19.5. Эффект от фильтра результатов Создание асинхронного фильтра результатов Интерф ейс IAsyncResul tFil te r используется для создания асинхронных филь­ тров результатов. Далее приведено определение этого интерфейса: using Systern . Threading . Tasks ; narnespace Microsoft . AspNetCo r e . Mvc .Filters puЫic interface IAsyncResultFilter : IFilterMetadata Task OnResultExecutionAsync(Resu ltExecut i ngContext context, ResultExecutionDelegate next); Интерфейс I AsyncResul tFi 1 ter похож на интерфейс , предназначенный для асин ­ хронных фильтров действий. В листинге 19.18 класс ViewResul tDetailsAttribute переписан с целью реализации интерфейса IAsyncResul tFil ter. Листинг 19.18. Создание асинхронного фильтра результатов в файле ViewResul tDetailsAttribute. cs using Systern . Co l lections.Generic; using Systern . Threading . Tasks ; using Microsoft.AspNetCore.Mvc ; using Microsoft . AspNetCore.Mvc.Filters ; using Mi crosoft.AspNetCore . Mvc .M odelBinding ; using Microsoft . AspNetCore . Mvc . Vi ewFeatures ; namespace Filters . Infrastructure { puЫic class ViewResu ltDet ailsAttribute : ResultFilterAttribute puЫic override async Task OnResultExecutionAsync( ResultExecutingContext context, Resu1tExecutionDe1egate next) {
596 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Di ctionary<s tring , string> dict = new Di cti onary<string , string> { ["Result Туре " ] = context .Res ult.GetType() .Name, }; Vi ewResult vr; if ((vr = context.Result as ViewResult) != nul l) { dict["View Name "] = vr .ViewName; dict[ "Мodel Туре" ] vr . ViewData . Model .GetType() . Narne ; dict [" Model Data " ] = vr . Vi ewData . Model . ToString() ; context . Resul t = new ViewResu lt { ViewNarne = " Message ", }; ViewData = n ew ViewDataD i cti o nary ( n ew ErnptyModelMetadataProvide r () , new ModelStateDictionary()) { Model = di ct await next (); Обратите внимание, что вы несете ответственность за вызов делегат а , получен­ ного в виде аргумента методом OnR e sul tExecutionAs ync () . Если не вызвать деле ­ гат, тогда конве йер обработки запросов не будет з авершен и р езульт ат дейс твия н е ви зуализируется . Создание гибридного фильтра действий/результато в Проводить различи е между стадиями действия и р езул ьтата процесса обр аботки запросов не всегда удобно. Так может случиться из - за того , что вы хотите трактовать обе стадии как один шаг, либо потому, что ваш фильтр реагирует на способ выпол­ нения действия , но делает это, вмешиваясь в результат. Полезно располагать воз­ можностью создания фильтра , который является фильтром одноврем енно действий и фильтром результатов и способен выполнять работу на каждой стадии. Требование настолько распространенное, что класс ActionFi l t e rAttribute р е ­ ализует интерф ейсы для обеих разновидностей фильтров, т. е . в единственном атрибу­ те можно смешивать и сочетать типы фильтров. Для демонстрации в листинге 19.19 приведен пересмотренный класс Pr ofileAttribute, в котором фильтр дей ствий объединяется с фильтром результатов. Листинг 19.19. Создание гибридного фильтра в файле ProfileAttribute. cs usi ng Systern . Di agnostics ; usi ng Systern . Text; using System . Threading . Tasks ; us i ng Microsof t . AspNetCore . Mvc . Filters ; narnespace Filters .I nfrastr u ctur e puЫic class Pr ofil eAttribute ActionFil terAtt ribute private Stopwatch timer; private douЫe actionTime;
puЫic override async Task OnActionExecutionAsync( ActionExecutingContext context , ActionExecutionDelegate next) { timer = Stopwatch.StartNew(); await next () ; actionTime = timer.Elapsed.TotalMilliseconds; puЫic override async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) { await next () ; timer.Stop(); string result = 11 <div>Action time: 11 + $ 11 { actionTime} ms</div><div>Total time: 11 Глава 19 . Фильтры 597 + $"{timer.Elapsed.Tota1Milliseconds} ms</div>"; byte [] bytes = Encoding. ASCII. GetBytes (resul t) ; await context.HttpContext.Response.Body.WriteAsync(Ьytes, О, bytes.Length); Здесь асинхронные методы применяются для фильтров обоих типов, но вы можете получить требуемую функциональность за счет смешивания и сочетания, поскольку станда ртные ре ализ ации этих методов вызывают свои синхронные аналоги. Внутри фильтра с помощью Stopwatch измеряется время выполнения действия и общее вре­ мя, а результаты записываются в ответ. В листинге 19 .20 комбинированный фильтр прим еняется к контроллеру Horne. Листинг 19.20. Применение гибридного фильтра в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using Filters . Infrastructure ; namespace Filters . Control l ers [Profile] [ViewResultDetails] puЫic class HomeController : Control l er { puЬlic ViewResul t Index () => View ( 11 Message ", " This is the Index action on the Home controller " ) ; puЫic ViewResul t SecondAction () => View ( " Message ", " This is the SecondAction action on the Home cont rol l er " ) ; Запустив приложение, вы увидите вывод , подобный показанному на рис . 19.6 . Вывод появля ется после содержимого, предоставляемого атрибутом ViewResul tDetails, т.к . он записывается на стадии постобработки фильтра результатов, а не получается из метода фильтра действий, который использовался в пр едыдущей версии.
598 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Name Value Resutt Туре ViewResult View Name Message Model Туре String Model Data This is the lndex action on the Ноте controller Action time : 0 .2576 ms Total time: 2.1627 ms ____________________J Рис. 19.6 . Вывод из гибридного фильтра действий/результатов Использование фильтров исключений Фильтры исключений позволяют реагировать на исключения без необходимости в написании блоков try ... catch в каждом методе действия. Фильтры исключений могут применяться к классам контроллеров или к методам действий. Они вызывают­ ся, когда исключение не обработано методом действия либо фильтрами действий или результатов , которые были применены к методу действия. (Фильтры действий и ре­ зультатов могут иметь дело с необработанным исключением, устанавливая свойство ExceptionHandled своих объектов контекста в true.) Фильтры исключений реализу­ ют интерфейс IExceptionFilter, который определен следующим образом: nam e space Micros o ft.AspNetCore.Mvc.Filters { puЫic interface IExceptionFilter : IFilterMetadata void OnException(ExceptionContext context); Метод OnException () вызывается при столкновении с необработанным исклю­ чением. Интерфейс IAsyncExceptionFil ter может использоваться для создания асинхронных фильтров исключений, которые удобны, когда необходимо реагировать на исключения с применением асинхронного АРI-интерфейса. Вот определение ин­ терфейса I AsyncExcep t i o nFil ter: using System .Threading.Tasks; namespace Microsoft.AspNetCore .M vc .Filters puЫic interface IAsyncExceptionFilter : IFil te rMetadata Task OnExceptionAsync(ExceptionContext context);
Глава 19 . Фильтры 599 Метод OnExcept i o n A s y n c () является асинхронным аналогом метода On Except i o n () из интерфейса I E xcep t i o nFil t er и вызывается при наличии необ­ работанного исключ ения. Для обоих интерфейсов данные контекста предоставляются чер ез кл асс ExceptionCon t e xt, производный от Fi l terContext , в котором опреде­ лены дополнительные свойства, описанные в табл. 19. 1О . Таблица 19.10. Свойства ExceptionContext Имя Описание Exception Это свойство содержит любой объект Except i o n, который был сгенерирован ExceptionDispatchinfo Это свойство возвращает объект ExceptionDi spatchinf o , содержащий информацию трассировки стека для исключения Except i onHandl ed Это свойство типа boo l используется для указания, обработа ­ но ли исключение Result Это свойство устанавливает объект реализации I ActionRe s u l t , который будет применяться для генерации ответа Создание фильтра исключений Класс Ex c eptionFilterAttrib u te реализует оба интерфейса фильтров исклю­ чений и является самым легким способом создания фильтра, так что он может быть применен как атрибут. Наиболее распространенное использование фильтра исключ е­ ний свя зано с отображением специальной страницы ошибки для специфического типа исюпоч ения , снабжая пользователя более полезной информацией, чем способны обес­ печить стандартные возможности обработки ошибок . Чтобы взглянуть на это, добавь­ те в папку Inf r astructure файл клас са по имени RangeExceptionAttribut e . cs и определите в нем фильтр , как продемонстрировано в листинге 19 .21 . Листинг 19.21. Содержимое файла RangeExceptionAttribute. cs из папк и Infrastructure using System ; us ing Microsoft .AspNetCor e . Mvc ; using Microsoft . AspNe t Core . Mvc . Filte r s; us i ng Microsoft . AspNetCore. Mv c . Mode lBind i ng ; using Microsoft . AspNetCo r e . Mvc .Vi ewFeatu r es ; namespace Fi lters . Infrastructure { puЫic class RangeExceptionAttribut e : Excepti onFi l terAttribu t e puЫic overri d e void OnException (E xcepti onCo n text context) { if (context . Exception is ArgumentOu tOfRangeException) { context .Resul t = new Vi ewRe sult() { Vi ewN ame = " Me ssage", ViewData = new Vi ewDataDictionary ( new EmptyModelMe t a d ata P rovi de r() , new Mode l StateDictionary()) { Model = @" The data r eceived Ьу the application cannot Ье processed " ) };
600 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Этот фильтр применяет объект ExceptionCon t ex t для получения типа необра­ ботанного исключ ения. Если типом является Argumen t OutOfRangeExcep tion , то создается результ ат действия, который отобр аж ает сообщение пользователю . В лис­ тинге 19.22 в контролл ер Ноте добавляется метод действия , а к 1юнтроллеру приме­ няется фильтр исмюч ений. Листинг 19.22. Применение фильтра исключений в файле Homecontroller. cs usi n g Fi lters .In frastructure ; using Mic r osoft . AspNetCore . Mvc ; using System; naтespace Fi l ters.Controllers [Profile] [ ViewResu l tDetails] [RangeException] puЫic class HoтeController : Controlle r { puЫic Vi ewResult I ndex() => View("Message", " This is the Index acti on on the Ноте control ler " ) ; puЫic ViewResult SecondAction () => Vi ew ( "Message ", "This is the SecondAction action on t h e Ноте controller " ) ; puЬlic ViewResult GenerateException(int? id) { if (id == null) { throw new ArgwnentNullException(nameof(id)); elseif(id>10){ throw new ArgumentOutOfRangeException(nameof(id)); else { return View ( "Message", $ "The value is {id} ") ; При получении из URL запроса значения int, допускающего nul l, метод де й ствия GenerateExcept i on () полагается на стандартный шаблон маршрутизации . Этот метод действия генерирует исключение Ar gume n tNul l Exception, е сли соотв етству­ ющий сегм ент URL отсутствует, и исмючение ArgumentOu tO f RangeException , если его значение больше 10 . При наличии сегмента со значением менее 10 метод дейс­ твия возвращает объект ViewRes u lt . Протестировать фильтр ис ключ ений можно , з апустив прило же ни е и з апросив URL вида /Home/Ge n erateException/ 1 00. Значение в последнем сегменте выходит за пределы, ожидаемые методом действия. который сгенерирует исмючение с типом , об­ р абатываемым фильтром, что даст ре зультат, представленный на рис . 19.7 . В случае запроса /Home/GenerateException метод действия сгенерирует исключение, которое не будет обработано фильтром, по этому задействуется стандартная обработка ошибок. И спользо вание в недрения за в исимостей для фильтров Когда фильтр делается производным от одного из удобных классов атрибутов, та ­ !{ИХ как Ex ceptionFilterAttr i bute , для обработки каждого запроса инфрас трукту­ ра MVC создает новый экземпляр класса фильтра.
Глава 19. Фильтры 601 Рис. 19.7. Использование фильтра исключений Это рациональный подход, поскольку он позволяет изб ежать возможных проблем, свя з анных с повторным использованием или параллелизмом, и удовлетворяет нуж­ дам большинства классов фильтров, которые требуются разработчикам. Альтернативный подход предусматривает применение системы внедрения зависи­ мостей с целью выбора другого жизненного цикла для фильтров. Использовать внед­ рение зависимостей в фильтрах можно двумя способами, которые будут описаны в последующих разделах . Распоз навание зависимостей в фильтрах Первый подход заключается в применении внедрения зависимостей для управле­ ния данными контекста, которые предназначены фильтрам. Это позволяет фильтрам разных типов совместно использовать данные или одиночному фильтру разделять данные между своими экземплярами, применяемыми для обработки других запросов. Чтобы посмотреть, как все работает, добавьте в папку In fra structure файл класса по имени F ilt e r Diagn o s tics. cs с определениями интерфейса и класса реализации из листинга 19.23. Листинг 19.23. Содержимое файла Fil terDiagnostics. cs из папки Infrastructure using Syst em . Collections . Gener ic; namespace F ilter s.Inf ras tru ct ur e { puЬlic int e r face I FilterDi ag n o stics IEnumeraЫe< string > Messages { get; voi d AddMessage(string me ssa g e ); puЫic class De fault Fi l terDiagnostics : IFil t erDi agnostics privat e List <string> me ssages = n e w List <string>() ; puЫic IEnumeraЫe<string> Messages => mess ages; puЫic void Add Me ss age(s tring me ssag e) => messages . Add(mess age) ; Интерфейс IFilterDia gnosti c s определяет простую модель для сбора диагности­ ческих сообщений во время выполнения фильтра. Класс Defa ul tFilt erDi agnost i c s является реализацией. которая будет использоваться. В листинге 19.24 показан класс S t artup , обновленный для конфигурирования поставщика служб с целью примене­ ния нового инте рфейса и его реализации.
602 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 19.24. Конфигурирование поставщика служб в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions.Dependency l njection ; using Filters.Infrastructure; namespace Filters { puЫic class Startup puЫic void ConfigureServices(ISe rviceCollection services) { services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>(); services . AddMvc() ; puЫic void Configure(IAppl i cationBuilder арр) { app.UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app . UseMvcWithDefaultRoute() ; Поставщик служб конфигурируется с использованием расширяющего метода AddScoped () , т.е. все фильтры, создаваемые для работы с одиночным запросом, по­ лучат тот же самый объект Defaul tFil terDiagnostics . Это является основой для разделения специ альных данных контекста между фильтрами. Соэдание фильтров с зависимостями На следующем шаге создаются фильтры , которые объявляют зависимости от ин­ терфейса IFilterDiagnostics . Добавьте в папку Infrastructure файл класса по имени TimeFilter . cs с содержимым, приведенным в листинге 19.25. Листинг 19.25. Содержимое файла TimeFil ter. cs из папки Infrastructure using System . Diagnostics; using System . Threading.Tasks ; using Microsoft . AspNetCore . Mvc.Filters; namespace Filters . Infrastructure { puЫic class TimeFil ter : IAsyncActionFil ter, IAsyncResul tFil ter { private Stopwatch timer ; private I FilterDiagnostics diagnostics ; puЫic TimeFilter(IFilterDiagnostics diags) diagnostics = diags; puЬlic async Task OnActionExecutionAsync( ActionExecutingContext context , ActionExecutionDelegate next) { timer = Stopwatch . StartNew() ; await next() ; diagnostics . AddMessage($@ "A ction time : {timer . Elapsed . TotalMilliseconds} " ) ;
puЫic async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) { await next () ; tirner.Stop(); diagnostics.AddMessage($@"Result tirne: (timer.Elapsed.TotalMilliseconds}"); Глава 19. Фильтры 603 Класс TimeFil ter представляет собой гибридный фильтр действий/результатов, который воссоздает функциональность таймера из предыдущего примера, но хранит информацию о времени с применением реализации интерфейса IFil terDiagnostics, которая объявлена нак аргумент конструктора и будет предоставлена системой внед­ рения зависимостей при создании фильтра. Обратите внимание, что класс TimeFilter явно реализует интерфейсы фильтров, а не наследуется от удобного класса атрибута. Вы увидите, что фильтры, которые опираются на внедрение зависимостей, применяются через другой атрибут и не ис­ пользуются для декорирования контроллеров или действий напрямую. Чтобы взглянуть, каким образом фильтры задействуют внедрение зависимостей для разделения данных контекста, добавьте в папку Infrastructure файл клас ­ са по имени DiagnosticsFil ter. cs и определите в нем фильтр, показанный в листинге 19.26. Листинг 19.26. Содержимое файла DiagnosticsFil ter. cs из папки Infrastructure using Systern.Text; using System.Threading.Tasks; using Microsoft.AspNetCore . Mvc.Filters; narnespace Filters.Infrastructure { puЬlic class DiagnosticsFilter : IAsyncResultFilter private IFilterDiagnostics diagnostics; puЫic DiagnosticsFilter(IFilterDiagnostics diags) { diagnostics = diags; puЬlic async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next) { await next(); foreach (string rnessage in diagnostics?.Messages) byte [] bytes = Encoding . ASCII .GetBytes($" <div>{rnessage}</div>"); await context.HttpContext . Response.Body .WriteAsync(bytes , О, bytes.Length);
604 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Класс DiagnosticsFil ter - это фильтр результатов, который получает реализа­ цию интерфейса IFil terDiagnostics в виде аргумента конструктора и записывает содержащиеся в ней сообщения в ответ. применение фильтров Финальный шаг заключается в применении фильтров к классу контроллера. Стандартные атрибуты С# не располагают всеобъемлющей поддержкой для распоз­ навания зависимостей конструктора, из-за чего фильтры, которые были определены в предшествующих разделах , не являются атрибутами. Взамен применя етс я атрибут TypeFilter, сконфигурированный с необходимым типом фильтра (листинг 19.27) . Совет. Порядок применения фильтров в листинге 19 .27 важен, как будет объясняться в разде­ ле "Порядок применения фильтров и его изменение " далее в главе . Листинг 19.27 . Применение фильтров с зависимостями в файле HorneController. cs using Microsoft . AspNetCore . Mvc; using Filters .I nfrastructure ; using System; namespace Filters . Controllers [TypeFilter(typeof(DiagnosticsFilter))] [TypeFilter(typeof(TirneFilter))] puЫic class HomeController : Controller puЫic ViewResult Index() => View("Message", " This is the Index action on the Home controller " ) ; puЬlic ViewResul t SecondAction () => View ( " Message ", " This is the SecondAction action on the Home controller " ) ; puЫic ViewResult Generate Exception(int? id) { if (id == null) { throw new ArgumentNullException(nameof(id)) ; elseif(id>10){ throw new ArgumentOutOfRangeException(nameof(id)) ; else { return View ("Message", $ "The value is {id}"); Атрибут TypeFil ter создает новый экземпляр класса фильтра для каждого запро­ са, но делает это с использованием средства внедрения зависимостей, которое поз­ воляет создавать слабо связанные компоненты и помещать объекты, участвующие в распознавании зависимостей, под управление жизненным циклом. В текущем примере сказанное означает, что оба фильтра , примененные в лис­ тинге 19.27, получат тот же самый объект реализации IFilterDiagnostics, а сообщения, сохраненные классом TimeF il ter, будут за писаны в ответ классом DiagnosticsFi l ter. Запустив приложение и запросив стандартный для него URL. можно увидеть результат (рис. 19.8).
This is the lndex action on the Ноте controller I Action time: 0.0114 1 Resulttime: О . 1561 Глава 19. Фильтры 605 Рис. 19.8. Использование фильтров с зависимостями Управление жизненными циклами фильтров В случае применения атрибута TypeFil ter новый экземпляр класса фильтра со­ здается для каждого запроса. Такое поведение ничем не отличается от поведения при использовании фильтра напрямую как атрибута за исключ ением того , что атрибут TypeFil ter позволяет массу фильтра объявлять зависимости, которые распознают­ ся посредством поставщика служб. Атрибут ServiceF il ter продвигается на шаг дальше и применяет поставщик служб для создания объекта фильтра. Это также дает возможность помещать объек­ ты фильтров под управление жизненным циклом. В целях демонстрации в листин­ ге 19.28 приведен модифицированный класс TimeFilter, который сохраняет сред­ нее арифметическое записанных значений времени. Листинг 19.28. Сохранение средних арифметических значений в файле TimeFilter. св using System . Collections . Concurrent ; using System . Diagnostics ; using System.Linq; using System . Threading . Tasks ; using Microsoft . AspNetCore . Mvc . Filters ; namespace Filters . Infrastructure { puЫic class TimeFilter : IAsyncActionFilter, IAsyncResultFilter private ConcurrentQueue<douЫe> actionTimes new ConcurrentQueue<douЫe>(); private ConcurrentQueue<douЫe> resultTimes new ConcurrentQueue<douЫe>(); private IFi lterDiagnos ti cs diagnos t ics ; puЬlic TimeFilter(IFilterDiagnostics diags) diagnostics = diags ; puЫic async Ta sk OnActionExecut i o nA sync( ActionExecutingContext context , ActionExecutionDelegate next) { Stopwatch timer = Stopwatch.StartNew(); await next() ; timer . Stop() ; actionTimes.Enqueue(timer.Elapsed.TotalМilliseconds);
606 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC diagnostics . AddMessage($@"Action time : {timer.Elapsed.TotalMilliseconds} Average: { actionTimes. Average () : F2} ") ; puЫic async Task OnResultExecutionAsync( ResultExecutingContext context , ResultExecutionDelegate next) { Stopwatch timer = Stopwatch.StartNew(); await next (); timer.Stop(); resultTimeз.Enqueue(timer.Elapsed.TotalMilliseconds); diagnostics . AddMessage($@ " Result time : {timer . Elapsed .T otalMi l liseco nd s} Average: {resultTimes.Average() :F2}"); Фильтр те перь использует безопасную к потокам коллекцию для хранения значе ­ ний времени, которые он записывает для стадий действия и р езультата обработки запроса, и прим еняет отдельный объект Stopwatch каждый раз , когда по ступает тре­ бовани е обработать зап рос. В листинге 19.29 класс TimeFilter регистрируется как одиночка с помощью поставщика служб в классе Startup . Листинг 19.29. Конфигурирование поставщика служб в файле Startup. сз using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions . Dependencyinjection ; using Filters.Infrastructure ; namespace Filters { puЫic class Startup puЫic void ConfigureServices(IServiceCollection services) { зervices.AddSingleton<IFilterDiagnostics, DefaultFilterDiagnostics>(); services.AddSingleton<TimeFilter>(); services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvcWithDefaultRoute() ; Обратите внимание, что жизненный цикл для класса реализации IFilterDiagnostics также изменен, чтобы он стал одиночкой. Если бы сохранилось создан ие нового эк­ земпляра для каждого запроса, то объект-одиночка TirneFil ter получал бы раз ные объекты реализации IFilterDiagnostics от фильтра Diagnost i csFilter, который продолжил бы создаваться через атрибут TypeFil ter и создавался бы для каждого запроса.
Глава 19. Фильтры 607 Применение фильтра Последний шаг касается применения фильтра к контроллеру с использованием ат­ рибута ServiceType (листинг 19.30). Листинг 19.30 . Применение фильтра в файле HomeController. cs using Microsoft.AspNetCore.Mvc ; using Filters .I nfrastructure; using System; namespace Filters.Controllers [TypeFilter(typeof(DiagnosticsFilter) )] [ServiceFilter(typeof(TimeFilter))] puЫic class HorneController : Controller puЫic ViewResul t Index () => View ( " Message", "This is the Index action on the Home controller " ); puЫic ViewResult SecondAction() => View( " Message ", "This is the SecondAction action on the Home controller " ); puЫic ViewResult GenerateException(int? id) { if (id == null) { throw new ArgumentNullException(nameof (id)); elseif(id>10){ throw new ArgumentOutOfRangeException(nameof(id) ); else { return View( " Message" , $"The value is {id}"); Запустив приложение и запросив стандартный URL, можно увидеть результат. Поскольку для распознавания всех зависимостей используется единственный объ­ ект реализации интерфейса IFil terDiagnostics, набор отображаемых сообщений строится с каждым запросом (рис. 19.9). This is the lndex action on the Ноте controller Action time: 9.9636 Average: 9.96 Result time: 2442.0765 Average: 2442.08 / Action time: 0.0564 Average: 5.01 1 Result time: 2 .069 Average: 1222.07 Action time: 0 .012 Average : 3 .34 , Resulttime:. 0 .1501 Avera.ge: 814.7.7 . ~..1 Action time: 0 .0114 Average: 2.51 '~t·.:1. це· ~ ~ ...~~ •. . ' Рис. 19.9 . Применение поставщика служб для управления жизненным циклом фильтра
608 Часть 11 . Подробные сведения об инфраструктуре ASP.N ET Core MVC Создание глобальных фильтров В начале главы было указано, что вы можете применять фильтры к целому клас­ су контроллера, чтобы не применять их к индивидуальным методам действий. Глобальны е фш~ьтры продвигаются еще дальше и применяются один раз в классе Startup, после чего. как и следует из их названия, автоматически применяются к каждому методу действия в каждом контроллере внутри приложения. В качестве гло­ бального фильтра может использо ваться любой фильтр . Для примера добавьте в пап­ ку Infrastructure файл класса по имени ViewResul tDiagnostics. cs с определе­ нием фильтра, показанным в листинге 19.31. Листинг 19.31. Содержимое файла ViewResul tDiagnostics. cs из папки Infrastructure using Microsoft.AspNetCore.Mvc; using Microsoft . AspNetCore . Mvc.Filters; narnespace Filters . Infrastructure { puЫic class ViewResultDiagnostics : IActi onF ilte r { private IFilterDi agnostics diagnostics; puЫic ViewResu ltDiagnostics(IFilterDiagnostics diags) diagnostics = diags ; puЫic void OnActionExecuting(ActionExecutingContext context) 11 ничего не дел ать - метод в этом фильтре не используется puЫic void OnActionExecuted(ActionExecutedContext context) ViewResult vr ; if ( (vr = context .Result as ViewResult) != null) { diagnostics . AddMessage($ " View narne : {vr . ViewNarne} " ) ; d iagnosti cs . AddMessage($@ " Mode l type: {vr.V i ewData . Mode l.GetType() . Narne}") ; Фильтр использует объект реализации IFil terDiagnostics для сохране­ ния сообщений об имени представления и типе модели результатов действий ViewResul t . В листинге 19 .32 этот фильтр применяется глобально наряду с клас­ сом DiagnosticsFilter, на который он полагается пр и записи диагностических сообщений. Листинг 19.32. Регистрация гл обальных фильтров в файле Startup. cs u sing Microsoft . AspNetCore . Builder; using Microsoft . Extensions . Dependencyin j ection ; using Filters . Infrastructure ; n arnespace Filters { puЫic c lass Startup puЫic void ConfigureServices(IServiceCollection services) {
Глава 19. Фильтры 609 services.AddScoped<IFilterDiagnostics, DefaultFilterDiagnostics>(); services.AddScoped<TimeFilter>(); services.AddScoped<ViewResultDiagnostics>(); services.AddScoped<DiagnosticsFilter>(); serv ices.Add.Мvc() .Add.МvcOptions(options => { options.Filters.AddService(typeof(ViewResultDiagnostics)); options.Filters .AddService(typeof(DiagnosticsFilter)); }); puЬlic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages(); app.UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvcWithDefaultRoute(); Dюбальные фильтры настраиваются путем конфигурирования пакета служб МVС. В рассматриваемом примере для регистрации фильтров как глобальных использу­ ется метод MvcOptions . Fil ters . AddService (). Метод AddService () принимает тип . NЕТ, экземпляр которого будет создан с применением правил жизненного цикла , указанных где-то в другом месте метода ConfigureServices () . Жизненный цикл других типов фильтров изменен на ограниченный областью действия, так что новые экземпляры создаются для каждого запроса. В результате новые экземпляры филь­ тров ViewResul tDiagnostics и DiagnosticsFil ter создаются и применяются к каждому запросу, адресованному каждому контроллеру. Совет. Добавлять глобальные фильтры можно также с использованием метода Add () вмес­ то AddService () , который позволяет регистрировать объект фильтра в качестве гло­ бального фильтра, не полагаясь на внедрение зависимостей и поставщика служб. Метод Add () применяется в следующем разделе . Добавьте в папку Controllers файл класса по имени GlobalController . cs с определением контроллера из листинга 19.33 . Листинг 19.ЗЗ. Содержимое файла GlobalController. cs из папки Controllers using Microsoft.AspNetCore . Mvc; namespace Filters.Controllers { puЫic class GlobalController : Controller { puЬlic ViewResult Index () => View ( " Message ", "This is the global controller"); Ника~{ие фильтры к контроллеру Global не применялись, но после запуска при­ ложения и запрашивания URL вида /global отобразится вывод из двух глобальных фильтров (рис. 19.10).
610 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC r-:---~~----~~--~~~~~~-~~. С [ <D localhost:51000/global This is the global controller View name : Message Model type: String ----·---·--·--·----·-----·----- Рис. 19.1 О. Использование глобальных фильтров Порядок применения фильтров и его изменение Фильтры запускаются в специфической очередности: авторизации, действий и результатов. Но при наличии нескольких фильтров заданного типа порядок их при­ менения управляется областью действия, через которую фильтры были применены. Добавьте в папку Infrastructur e файл класса по имени MessageAttribute. cs и определите в нем фильтр, приведенный в листинге 19.34. Листинг 19.34. Содержимое файла MessageAttribute. cs из папки Infrastructure using System . Text; using Microsoft.AspNetCore.Mvc.Filters ; namespace Filters.Infrastructure puЬlic class MessageAttribute : ResultFilterAttribute private string message ; puЫic MessageAttribute(string msg) message = msg; puЫic override void OnResultExecuting(ResultExecutingContext context) WriteMessage(context, $"<div>Before Result : {message)</div>"); puЬlic override void OnRe sultExecuted (Res ult ExecutedContext context) WriteMessage(context, $"<div>After Result : {mes sage)</div> " ) ; private void WriteMessage(FilterContext context , string msg) { byte[] bytes = Encoding.ASCII . GetByte s($ " <div>{msg}</div> "); context.HttpContext . Response .Body . Wr ite (bytes , О, bytes.Length) ;
Глава 19 . Фильтры 611 В листин ге 19.34 определ ен фильтр результатов , который записывает фрагменты НТМL- разметки до и после обработки результата действия . Сообщение , записываемое фил ьтром , конфигурируется поср едством аргумента конструктора , который мож ет исполь з оват ься, 1<0гда применя ется ат рибут. В листинге 19.35 контроллер Home уп­ рощен, а фильтры из предш ествующих примеров заменены множеством экземпляров фильтр а Message . Листинг 19.35. При м енение фильтра в файле HorneController. cs using Microsoft . AspNetCore . Mvc ; using Filters . Infrastructure ; namespace Filters . Controllers { [Message("This i s t he Control ler- Scoped Fil t er")] puЫic class HomeCo ntrol ler : Controlle r { [Message ( "This is the First Action-Scoped Filter")] [Message("This is the Second Action-Scoped Filter")] puЫic ViewResul t Index () => View ( "Message ", "ThisistheIndex actionontheHome controller"); Н а бор глобальных фильтров изменен с целью исполь з ов ания также фильтра Message (л исти нг 19.36). Листинг 19.36. Создание глобального фильтра в файле Startup. cs using Microsoft . AspNetCore . Bui l de r; using Microsoft . Exten s ions . Dependency in je c t i on ; using Filters . Infras tru ctur e ; namespace Filters { puЫic class Startup puЫic void Confi gureSe r v i ces(IService Col l ection servi ces) { services . AddScoped<IFi lterDia g nostics , De f a ult Filt erDia gno s tics>() ; services . AddScoped<T i me F i l ter> (); services . Add Scoped<Vi ewRes ultDia gno stics> ( ) ; services . AddScoped<Diagnost i cs Fi l te r >() ; services . AddMvc() . AddMvcOptions ( op ti o n s => { options.Filters.Add(new MessageAttribute("This is the Globally-Scoped Filter")); )); puЫic void Confi gure(IAppli cation Builder арр) { app . OseStatusCodePages() ; app . UseDe v eloper Excep t ion Page (); app . OseStat icFi les( ); app . UseMvcWith De f aultRout e() ;
612 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC При реагировании метода действия I n d ex () на запрос будут использоваться че ­ тыре экземпляра фильтра . Запустив приложение и запросив стандартный URL , вы увидите следующий вывод , отображаемый в браузере : Before Result : This is the Globally- Scoped Filter Befo r e Result:This is the Contro lle r - Scoped Filter Before Res u lt:This is the First Action- Scoped Filter Before Result : This i s the Second Action - Scoped Filter Afte r Result : This i s the Second Action -S coped Fi lter After Result : This i s the Fi rst Action - Scoped Filter After Result : Th i s is the Controller - Scoped Filter After Resu l t : Th i s is the Gl obally- Scoped Filter По умолчанию инфраструктура MVC запускает глоб альные фильтры. затем филь­ тры, примененные к классу контроллера, и в заключ ение фильтры , которы е при мене ­ ны к методам действий. После того, как метод действия вызван ил и р е зультат дейс­ твия обработан, стек фильтров раскручивается, из - за чего сообщения After Resul t в выводе расположены в обратном порядке . Изменения порядка применения фильтро в Стандартный порядок м ожет быть изменен за счет реализаци и интерфейса I OrderedFilter, который MVC ищет при выяснении, как пом ещать фильтры в пос­ ледовательность. Вот определение этого интерфейса: name s pace Microsoft . AspNetCore . Mvc . Filters { pu Ы ic interface I OrderedFilte r : IFi lterMeta d ata int Order { get; } Свойство Order возвращает значение i nt ; низко е значени е указывает инфраструк­ туре MVC на необходимость применения фильтр а пер ед фильтрам и с бол ее высокими значениями Order . Удобны е атрибуты уже реализуют интерфейс IOrderedFilter, и в листинг е 19.37 свойство Ord er устанавливается для фильтров , прим ен енных к контроллеру Home . Совет. Атрибуты TypeFil ter и Servi ceFiltеrтоже реализуют интерфейс IOrderedFil ter, т. е. в случае использ о вания внедрения зависимостей порядок применения фильтров так­ же можно изменять. Листинг 19.37. Установка порядка применения фильтров в файле HomeController. cs using Fil ters .I nfrastructu r e ; usi ng Microsoft .AspNetCore.Mvc; n amespace Filters.Control lers { [Message("This is the Controller-Scoped Filter", Order = 10)] puЫic class HomeController : Con tro l ler { [Message ("This is the First Action-Scoped Filter", Order = 1)] [Message ("This is the Second Action - Scoped Filter", Order = -1 )] puЫic ViewResult Index() => View( "Message", " This is the Index action on th e Home controller " ) ;
Глава 19. Фильтры 613 Значения Orde r могут быть и отрицательными, что дает удобный способ гаранти­ ровать применение фильтра перед любыми глобальными фильтрами со стандартным порядком (хотя при создании глобальных фильтров можно также устанавливать поря­ док). Запустив приложение, вы увидите, что порядок следования сообщений в выводе изменился , чтобы отразить новые приоритеты: Before Result : This is the Second Act i on- Scoped Filter Before Result : This is the Globally- Scope d Filter Before Resu l t : This is the First Action- Scoped Filter Before Result : Th i s i s the Controll e r-S coped Filter After Resul t :This is t he Cont roll er-Scoped Filter After Result :This is the First Act ion- Scoped Filter After Result : Th i s is the Globally- Scop ed Filter After Result : This is the Secon d Act i o n- Scoped Filter Резюме В настоящей главе было показано, как инкапсулировать логику сквозной ответс­ тв енности в виде фильтров. Вы узнали о доступных типах фильтров и о том, как их р е ализовать. Были приведены объяснения, каким образом применять фильтры в форме атрибутов к контроллерам и методам действий, а также в качестве глобальных фильтров. В следующей главе будет поrшзано, как использовать контроллеры для со­ здания веб-служб.
ГЛАВА 20 Контроллеры API н е все контроллеры используются для отправки НТМL-документов клиентам. Существуют также контроллеры API, которые применяются для предоставл е ­ ния доступа к данным приложения. Это средство, которое ранее предлагалось через отдельную инфраструктуру Web API, но теперь оно интегрировано в ASP.NET Core МVС. В настоящей главе объяснена роль контроллеров API в веб-приложении, опис а­ ны задачи. которые они решают, и продемонстрированы способы их создания, тес­ тирования и использования . В табл . 20. l приведена сводка, позволяющая поместить контроллеры АР! в контекст. Таблица 20.1. Помещение контроллеров API в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие­ то с к рытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией MVC 5? Ответ Контроллеры API подобны обычным контроллерам за исключени­ ем того, что ответы , порождаемые их методами действий - это объекты данных, которые отправляются клиенту без НТМL­ разметки Контроллеры API позволяют клиентам иметь доступ к данным в приложении, не получая также и НТМL-разметку, которая требу­ ется для представления содержимого пользователю. Не все кли­ енты являются браузерами , и не все клиенты отображают данные пользователям. Контроллер API делает прило жение открытым для поддержки новых типов клиентов или клиентов, разработан­ ных третьей стороной Контроллеры API применяются аналогично обычным контролле­ рам HTML Наиболее распространенные проблемы связаны со способом сериализации объектов данных, чтобы их можно было отправить клиенту. За подробными сведениями обращайтесь в раздел "Форматирование содержимого" далее в главе Вы не обязаны использовать контроллеры API в свои х проектах , однако их применение может увеличить ценность вашей плат­ формы для клиентов Контроллеры API ранее предоставлялись через инфраструктуру Web API, но теперь они интегрированы в ASP.NET Саге MVC и со­ здаются подобно обычным контроллерам В табл. 20.2 приведена сводка для этой главы.
Таблица 20.2 . Сводка по главе Задача Предоставление доступа к дан­ ным в приложении Запрос данны х из контроллера API Предоставление клиентам неко­ торого диапазона разных форма­ тов данных Переопределение процесса со­ гласования содер ж имого Разрешение клиентам переопре­ делять заголовок Accept за счет указания формата данны х в URL Предоставление полной подде­ ржк и для процесса согласования содерж имого Получение данных в разнообраз­ ны х форматах , используя разные методы действий Глава 20. Контроллеры АР! 615 Решение Листинг Создайте контроллер АР! 20.1 -20 .11 Используйте запрос Ajax , либо напрямую 20 .12-20.14 работая с АРl-интерфейсом браузера, либо через библиотеку вроде jQuery Добавьте в проект MVC дополнительные 20.16-20 .17 пакеты сериализации Применяйте атрибут Produces 20.18 Добавьте в класс Startup отображения 20.19-20.20 форматеров, добавьте переменную сег- мента, которая за х ватывает формат дан- ных, и дополнительно примените атрибут ForrnatFilter Включите форматер Ht tpNotAcce 20.21-20.22 ptaЬleOutputForrnatter и уста - новите конфигурационное свойство RespectBrowserAcceptHeader Примените атрибут Consumes 20 .23 Подготовка проекта для примера Создайте новый проект типа Empty (Пустой) по имени ApiControllers с исполь­ зованием шаблон а ASP.NET Саге Web Applicatioп (.NET Саге) (В е б-приложе ни е ASP.NET Core (.NET Core)). Создание модели и хранилища Начните с создания папки Models , добавьте в не е файл класса по имени Reservation . cs и определите класс модели, как показано в листинге 20.1. Листинг 20.1 . Содержимое файла Reservation. cs из папки Models namespace ApiControllers . Models { puЬlic class Reservation { puЫic int Reservationid { get ; set ; puЫic string ClientName { get ; set ; puЬlic string Location { get ; set ; }
61 б Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Затем добавьте в папку Models файл класса по имени I Reposi tory . cs и опреде­ лите в нем интерфейс для хранилища объе1tтов м одели (листинг 20.2). Листинг 20.2 . Содержимое файла IReposi tory. cs из папки Models using System . Col l ections.Generic ; namespace ApiControllers.Models puЫic interface IRepository { IEnumeraЬle<Reservation> Reservations { get ; } Reservation t his [int id] { get ; } Reservation AddReservation(Reservation reservation); Reservation UpdateReservation(Reservation reservation); void DeleteReservation(int id}; Далее добавьте в п апку Models файл 1wacca по имени MemoryReposi tory. cs с оп­ ределением непостоянной реализации интерфейса IReposi tory из листинга 20 .3 . Листинг 20.З. Содержимое файла MemoryReposi tory. cs из папки Models using System . Collections . Generic; namespace ApiControllers . Models { puЬlic class MemoryRepository : IRepository private Dictionary<int, Reservation> items ; puЫic MemoryRepository() { items = new Dictionary<int , Reservation>() ; new List<Reservation> { new Reservation { ClientName new Reservation { ClientName new Reservation { ClientName "Alice", Location = "Board Room" }, " ВоЬ ", Location "Lecture Hall" }, "Joe", Location = "Meeting Room 1" } . ForEach (r => AddReservation (r)) ; puЫic Reservation this[int id] => items.ContainsKey(id) ? items[id] puЬlic IEnum eraЫe<Reservation> Reservations => items . Values ; puЫic Reservation AddReservation(Reservation reservation) { if (reservation . Reservationid == 0) ( int key = items.Count ; while (items . ContainsKey(key)) key++ ; }; reservation . Reservationld = key ; items[reservation . Reservationid] return reservation; reservation; puЫic void DeleteReservation(int id) => items . Remove(id) ; puЫic Reservation UpdateReservation(Reservation reservation) => AddReservation(reservation); null ;
Глава 20. Контроллеры API 617 При создании экземпляра хранилища строится простой набор объектов модели, а поскольку постоянство не поддерживается, то в случае останова или перезапуска приложения все внесенные изменения будут утрачены. (Пример создания постоянно­ го хранилища приводился в главе 8 как часть проекта приложения SportsStore.) Создание контроллера и представлений По зже в главе будут созданы контроллеры REST, но при подготовке нужно создать обычный контроллер, чтобы обеспечить основу для дальнейших примеров. Создайте папку Controllers , поместите в нее файл класса по имени HomeController . cs и опр еделите в нем контроллер, код которого приведен в листинге 20.4 . Листинг 20.4 . Содержимое файла HomeController. св из папки Controllers usi ng Microsoft.AspNetCore .M vc ; using Api Controllers . Models; namespace Ap iControl lers .Controllers puЫic class HomeController : Controller private IRepository repository { get; set; puЫic HomeCont rol ler(IRepository repo) { repos itory = repo; puЫic ViewResult Index() => View(repository.Reservations); [Http Post] puЫic IActionResult AddReservation(Reservation reservation) repository . AddReservation(reservation) ; return RedirectToAction( " Index"); В контроллере определено действие Index, которое является стандартным для приложения и визуализирует модель данных. В нем также определено действие AddReservation, которое доступно только НТГР-запросам POST и применяется для получения данных формы от пользователя. Упомянутые действия следуют паттерну Post/Redirect/Get, описанному в главе 17, так что перезагрузка веб-страницы небу­ дет создавать повторную отправку формы. Чтобы можно бьmо отделить НТМL-содержимое от заголовка документа, необходимо создать компоновку, которая упростит изменения. вносимые позже в главе. Создайте папку Views/Shared и добавьте в нее файл компоновки по имени _Lауоut. cshtml с содержимым, показанным в листинге 20.5 . Листинг 20.5 . Содержимое файла_Layout. cshtml из папки Views/ Shared < !D OCTYPE html> <html> <head> <meta name ="viewport " content= " width=device-width" /> <title>RESTfu l Contro llers </title> <link asp -href- inc l ude= "liЬ/b ootstrap/dist/css/*.min .css" rel= " styl esheet" /> </head> <body class= "panel-body"> @RenderBody () </body> </html>
618 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Теп ерь создайте папку Views/Home , добавьте в нее файл представления по имени Index . cshtml и поместите в него содержимо е из листинга 20.6 . Листинг 20.6. Содержимое файла . cshtml из папки Views/Home @mod e l IEnumeraЬle<Reservation> @{ Layout = " Layout "; } <form id= " addform " asp-acti on= " AddReservation " method= " post " > <div class= " form - group " > <l abel for= " ClientName " >Name : </ l abel> <input class= " form-control " name= " Cl ientName " /> </div> <div class=" form- group " > <label for= "Location " >Location:</label> <input class= " form-control " name =" Location" /> </div> <div class="text- center panel - body" > <button type= " submit " class= " btn btn-sm btn-primary " >Add</button> </div> </form> <tаЫе class= " taЫe taЫe - condensed taЬle - striped taЫe - bordered " > <thead><tr><th>ID</th><th>Client</th><th>Location</th></tr></thead> <tbody> @foreach (var r in Model) { <tr> <td>@r . Reservationid</td> <td>@r . ClientName</td> <td>@r . Location</td> </tr> </tbody> </tаЫе> Это стр ого типизированное представление получает последовател ьно сть объектов Reservation в качестве своей модели и с помощью Rаzоr-цикл а foreach запол няет ими таблицу. Предусмотр ен также элемент form , который сконфигурирован для от­ правки запросов POST к действию AddReservation. Примеры в настоящей главе зависят от СSS-пак ета Bootstrap. Чтобы добавить Bootstrap в проект, создайте в корневой папке проект а файл bower . j s on с использо­ ванием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и до­ бавьте пакет Bootstrap в ра здел dependencies этого файла (листинг 20.7) . Листинг 20. 7. Добавление пакета в файле bower. j son "name ": "asp . net ", " private ": true , " dependencies ": { "bootstrap": 11 3.3 .6 11
Глава 20. Контроллеры API 619 Создайте в папке Views файл _Viewimports . cshtml и настройте в нем встроен­ ные дескрипторные вспомогательные классы для применения в представлениях Razor, а также обеспечьте импортирование пространства имен модели (листинг 20.8). Листинг 20.8 . Содержимое файла_Viewirnports. cshtrnl из папки Views @using ApiControllers . Models @addTagHelper * , Microsoft . AspNetCore . Mvc.TagHelpers Конфигурирование приложения Доб ав ьте в ра здел dependen cies файла proj ect . j son требуемы е пакеты NuGet и настройте в разделе tools инструментарий Razor, как демонстрируется в листин­ ге 20.9 . Разделы, которые не нужны для данной главы, понадобится удалить . Листинг 20.9 . Добавление пакетов в файле proj ect. j son " dependenc i es ": { "Microsoft.NETCore.App": "version": "1 .0 .0", " type ": " platform " }, }, "Microsoft .AspNetCore . Diagnostics ": "1 . О . 0", " Microsoft . AspNetCore . Server.IISintegration ": " 1 . 0 . О ", " Microsoft . AspNetCore . Server . Kestre l": "1. 0 . 0 ", " Microsoft . Extensions . Logging . Conso l e ": "1. 0 .0 ", "Microsoft.AspNetCore.Mvc": 11 1.0 . 0 11 , "Microsoft.AspNetCore.StaticFiles": "1.0 .0", "Microsoft.AspNetCore.Razor.Tools": "version": "1.0.0 -preview2-final", "type": "build" " tools ": }' "Microsoft . AspNetCore . Razor.Tools": "1 . 0.0 -preview2-final", "Microsoft . AspNetCore . Server . IISintegration . Tools": " l . 0 . 0 -preview2 -final " " frameworks ": { " netcoreappl.0 ": " imports ": ["dotnet 5 . 6 ", " portaЫe - net45+win8 "] }, " buildOptions ": " emitEntryPoint ": true , "preserveCompilationContext ": true }' " runtimeOptions ": { " configProperties ": { " System . GC . Server ": true }
620 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC В листинге 20.10 приведен класс Sta r tup, который конфигурирует средства, пре ­ доставляемые пакетами NuGet, и использует метод AddS ingleton () ,чтобы настро­ ить отображение службы для хранилища объектов модели. Листинг 20.1 О. Содержимое файла Startup. cs using Mi crosoft.AspNetCore .Builder ; using Microsof t. Ex t ensions . Dep e n d encyinj e ction ; using ApiControllers.Models; n amespace Ap i Co nt rollers { puЬlic cl ass Startup ( puЫic voi d Config ureServi ces (IServiceCollect ion servi c e s ) services.AddSingleton<IRepository, MemoryRepository>(); services.AddМvc(); puЫ ic v oid Configure (IApplicat ionBuil der арр) { a pp . UseStatusCode Pages() ; app.UseDevelope rExcept ionPage () ; app .UseStaticFil es () ; app . UseMv cWithDefau ltRoute ( ) ; Установка порта НТТР Н екоторые примеры в этой главе тестируются путем набора URL вручную. Чтобы облегчить опис ание , понадобится установить порт, который будет получать НТГР­ запросы. Выбе рите пункт ApiControllers Properties (Свойства ApiCont ro llers) в меню Project (Проект) среды Visual Studio, перейдите на вкладку Debug (Отладка) и изме­ ните значени е в пол е Арр URL (URL приложения) на http : / / localhost : 7000/, как показано на рис . 20.1 . После установки номера порта сохраните изменения. Запустите приложение, заполните форму и щелкните на кнопке Add (Добавить) ; приложение добавит в модель новый объект Re servat i o n (рис. 20.2). Вноси мые в хранилище изменения не будут постоянными, утрачиваясь при останове или п ере­ запуске приложения. Арр UR : [hitP:/11oca lhost:7000/ 0 EnaЫe SSL UR L: ~ ЕnаЫе Anonymous Aulhentication О ЕnаЫе \"Ji ndows Authentication Рис. 20.1. Установка URL приложения
Глава 20 . Контроллеры API 621 D i!ESTfulC0ttttol!~rs х ~ f\ES1fu l Coritrol:ers Jacqu1 !· :.m~~ ~C~~:'h~~~~~-~:-:= ~:~ ~-::;-:::~~~·-:::-=:._::~ J Namo; 1 Loci11io n: l ocalio n: Moo ting Rooni 7 ll__,.._ I~ ю Clicn l l ocalion j • ' Аl1св Вoard Room 1ID !1 ВоЬ Leclur• Hall l ocation ВoordRoom l. ~-----~~-------Moolin:oom ~1 ...-' ___:ь_._ __ ,.._____ ~ес-•~-~~-·н-R~-n_m_, ----- ---. Jacqui MeeUng Roo m 7 1 L.- - Рис . 20.2. Выполнение примера приложения Роль контроллеров REST ! 1 1 ! 1 __ •.... ! Рассматриваемое приложение является примером классического веб-приложения. Вся логика приложения находится на сервере и содержится в классах С# , что дела­ ет ее лепюй в управлении, тестировании и сопровождении. Но приложению, спроек­ тированному подобным образом, могут быть присущи серьезные недостатки в плане скорости, эффективности и открытости. Проблема скорости В настоящий момент пример приложения представляет собой сuнхрон.н.ое веб­ приложение . После того как пользователь щелкает на кнопке Add, браузер отправля­ ет серверу запрос POST, ожидает ответа и затем визуализирует полученную НТМL­ разметку. В течение этого промежутка вр емени пользователь ничего не может делать, а только ждать. Период ожидания может быть незначительным на этапе разработки, когда браузер и сервер располагаются на одной машине; тем не менее, развернутые приложения подпадают под реальные характеристики ограничений и задержек , по­ этому время, в течение которого синхронное приложение вынуждает пользователя ждать , может оказаться ощутимым. Синхронное приложение не всегда имеет проблему со скоростью. Например, если вы пишете производственное приложение для применения в единственном местопо­ ложении, где все клиенты подключаются через быструю и надежную локальную сеть, тогда проблема может не возникать. С другой стороны, если пишется приложение для мобильных клиентов на площадках со скудной инфраструктурой, то проблема скоро­ сти может быть существенной.
622 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Совет. Некоторые браузеры позволяют эмулировать разные типы сетей, что является удоб­ ным инструментом для выяснения, склонны ли пользователи согласиться на работу с син­ хронным приложением в определенном диапазоне сценариев. Скажем, браузер Google Chrome предлагает средство под названием сетевое регулирование, которое доступно в разделе Network (Сеть) инструментов, вызываемых по нажатию <F12>. Доступен диапа­ зон предопределенных сетей или же можно создать собственную сеть, указав скорости выгрузки и загрузки, а также задержку. Проблема эффективности Проблема эффективности проистекает из способа, которым синхронное веб-прило­ жение трактует браузер как механизм визуализации НТМL-разметки, используемый только для отображения отправляемых сервером НТМL-документов. Когда пользователь впервые запрашивает стандартный URL примера приложения, отправленный обратно НТМL-документ содержит все, в чем браузер нуждается для отображения содержимого приложения, включая перечисленную ниже информацию: • содержимое, основанное на СSS-файле Bootstrap, которое должно быть загруже­ но, если не доступна кешированная копия; • содержимое, имеющее в своем составе форму, которая сконфигурирована для отправки запроса POST к действию AddReservation; • содержимое, имеющее в своем составе таблицу, тело которой содержит три за­ полненных строки. Пример приложения прост, и начальный запрос приводит к тому, что сервер по­ сылает клиенту около 1,3 Кбайт данных. Однако когда пользователь отправляет форму, клиент перенаправляется снова на действие Index, в результате давая еще 1,3 Кбайт данных, чтобы отразить добавление одиночной строки таблицы. Браузер уже визуализировал форму и таблицу, но они были отброшены и заменены полно­ стью новым представлением того, что является практически совершенно тем же са­ мым содержимым. Может показаться, что объем в 1,3 Кбайт данных не особенно велик и, несомнен­ но, вы будете правы. Но если учесть соотношение между полезным и дублированным содержимым, тогда вы увидите, что подавляющее большинство данных, посылаемых браузеру, оказывается излишним. К тому же пример приложения намеренно упро­ щен; очень немногие реальные приложения требуют настолько мало НТМL-разметки, и по мере возрастания сложности приложения объем дублированного содержимого значительно увеличивается. Проблема открытости Последняя проблема традиционного веб-приложения связана с тем, что его про­ ектное решение закрыто, означая возможность доступа к данным в модели только через действия, которые предоставляет контроллер Home . Закрытые приложения ста­ новятся проблемой, если лежащие в основе данные необходимо использовать в дру­ гом приложении, особенно когда такое приложение разрабатывается другой коман­ дой или даже другой организацией. Разработчики зачастую уверены, что ценность приложения кроется во взаимодействиях, которые оно предлагает пользователям, в значительной степени из-за того, что они являются теми частями, на обдумывание и
Глава 20. Контроллеры API 623 реализацию которых разработчики тратят немалое время. Но как только приложение установилось и накопило активную пользовательскую базу, содержащиеся в нем дан­ ные часто становятся важными. Введение в REST и контроллеры API Контроллер API - это контроллер MVC, который отвечает за предоставление до­ ступа к данным в приложении, не инкапсулируя их в НТМL-разметку. Это позволяет извлекать или модифицировать данные в модели без необходимости в применении действий, предлагаемых обычными контроллерами, такими как контроллер Home в примере приложения. Самый распространенный подход к доставке данных из приложения предусмат­ ривает использование паттерна REST (Representational State Тransfer - передача со­ стояния представления). Детальная спецификация REST отсутствует, что привело к появлению многочисленных подходов, позиционирующих себя как согласованные с REST. Тем не менее, существует несколько унифицированных идей, которые полезны в клиентской части разработки веб-приложений. Основная предпосылка веб-службы REST связана с тем, чтобы заключать в себе характеристики НТГР, поэтому методы запросов (также называемые командами) ука­ зывают серверу операцию, подлежащую выполнению, а URL запроса определяет один или большее число объектов данных, к которым операция будет применена. В качестве примера взгляните на URL, который может ссылаться на специфичес­ кий объект Reservation в рассматриваемом приложении: /api/reservations/1 Первая часть URL - api - используется для отделения части данных приложе­ ния от стандартных контроллеров, которые генерируют НТМL-размет1<у. Следующая часть - reservations - указывает коллекцию объектов, с которой будет произво­ диться работа. Финальная часть - 1 - задает индивидуальный объект внутри коллек­ ции reservations. В примере приложения это значение свойства Reservationid, которое уникальным образом идентифицирует объект, и будет применяться в URL. Идентифицирующие объект URL объединяются с методами НТГР для указания операций. В табл. 20.3 перечислены наиболее часто используемые методы НТГР и описано, что они представляют, когда комбинируются с URL примера. Также приведе­ ны детали о том, какие данные (т. е . полезная нагрузка) включаются в запрос и ответ для каждой комбинации метода и URL. Контроллер АР!, который обрабатывает такие запросы, с помощью кода состояния ответа сообщает об исходе запроса. Следование соглашению REST не является требованием, но содействует упроще­ нию работы с приложением, потому что один и тот же подход широко прим еняется во многих уже установившихся веб-приложениях. Создание контроллера API Процесс создания контроллера АР! основан на подходе, используемом для стан­ дартных контроллеров, с рядом дополнительных средств, которые помогают указы­ вать АРI-интерфейс, представляемый клиентам. Добавьте в папку Controllers файл класса по имени ReservationController. cs с определением, приведенным в лис­ тинге 20.11 . Функциональность, предлагаемая этим контроллером, анализируется в последующих разделах.
624 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC Таблица 20.З. Объединение методов НТТР с UR L для указания веб - службы REST Ко м анда URL Описание Полезная нагрузка GET /api/reservations Эта комбинация из- Ответ содер жит пол- влекает все объекты ную колле кцию объе ктов Reservation GET /api /reser vations/ l Эта комбинация Ответ содержит указанный извле кает объект объект Reservation Reser vation, свойс- тво Reservat i on i d которого имеет зна- чение 1 POS T /api/reservation Эта комбинация со- За п рос содер жит значения здает новый объект для друг их свойств, которые Reservat i on требуются для создания объекта Reservation. Ответ содер жит объект, ко- торый был сохранен , гаран- тируя получение клиентом сохраненны х данны х PUT /api/reservation Эта комбинация Запрос содер жит значения , обновляет сущее- требуемые для изменения твующий объект свойств указанного объекта Reservation Reservat i on. Ответ со- дер ж ит объект, который был сохранен, гарантируя полу- чение клиентом сох раненны х данных DE LETE /api/reservation/1 Эта комбинация Полезная нагруз к а в запросе удаляет объект или ответе отсутствует Rese r vation, свойс- тво Reservationid которого имеет зна- чение 1 Совет. Вспомните , что классы контроллеров могут определяться где угодно в прое кте, а не только в папке Con t ro llers. Для крупных и сложных проектов мо жет быть удобно опре­ делять контроллеры API отдельно от обычных контроллеров HTML и помещать их в какую­ нибудь подпап ку и ли даже в выделенную папку. Листинг 20.11. Содер жи м ое файла ReservationController. cs и з папки Controllers using System . Collections . Gener i c ; u sing Microsoft . AspNetCore . Mvc ; using ApiControl l ers . Models ; namespace ApiControllers . Controllers [Route( " api/ [controller] " ) ] puЬlic cl ass Reser vationControll er : Controll er
Глава 20 . Контроллеры API 625 private IRepository repository ; puЫic ReservationControl l er(IRepository repo) { repository = repo ; [HttpGet] puЫic IEnumeraЫe<Reservation> Get( ) => repository . Reservat i ons ; [HttpGet ( 11 { id} 11 )] puЫic Reservation Get(int id) => repository[id ]; [HttpPost] puЫic Reservation Post( [ FromBody ] Reservation res) => repository . AddReservation (new Reservation { ClientName = res . Cli entName , Location = res . Location }); [HttpPut] puЫic Reservation Put( [FromBody ] Reservat i on r es) => repository . UpdateReservation(res) ; [HttpDelete ( " {id} " ) J puЬlic void Delete(int id) => repository . DeleteReservation(id) ; Контролле ры АР! работают в той же самой манер е , что и обычны е контроллеры. т. е . можно создать контроллер РОСО или унасл едовать класс от базового класса Controller, который пр едоставляет бол ее удобный доступ к данным контеJ{СТа запроса. Адаптаци я паттерна REST Паттерн REST поощряет определенную долю догматиз м а относительно того, каким образом АРl-интерфейсы веб-приложения дол ж ны быть представлены клиентам . Паттерн REST не является стандартным или хотя бы четко определенным , и существует несколько полезных подходов, которые облегчают адаптацию REST в приложениях ASP.NEТ Core MVC, но имеют тенденцию к крушению планов у тех программистов, кто обладает установившимися пред­ ставлениями о том, что считается соответствующим REST. В табл. 20.З URL , перечисленные для операций POS T и PU T , не идентифицируют ресурс уни кальным образом, что не которые считают неотъемлемой хара ктеристикой RESТ. В слу­ чае операции POST уникальный идентификатор объекта Reservation назначается моде­ лью, т. е . клиент не м ожет предоставить его как часть URL. В случае операции PUT средство привязки моделей MVC (которое описано в главе 26 и является причиной применения ат­ рибута FromBody в листинге 20 . 11) упрощает получение деталей подле жащего модифи­ кации объе кта Reserva tion из тела запроса . Таким образом, именно здесь контроллер Reservation ож идает найти значение Rese r vat i onid, идентифицирующее объект мо­ дели, который должен быть модифицирован . Подобно всем паттернам REST представляет собой отправную точку, предлагающую удоб­ ные и полезные идеи . Это не жесткий стандарт, который дол жен соблюдаться любой ценой ; единственная важная цель в том, чтобы писать понятный, тестируемый и сопровождаемый код. Учет природы прило жений MVC и проектного решения, положенного в основу храни­ лища , способствует получению более простого приложения и по-пре жнему предоставляет практичный АРl-интерфейс для потребления клиентами. Я советую рассматривать паттерны как руководящие принципы, которые вы подгоняете под собственные нужды, что справед­ ливо в отношении как REST, та к и MVC в целом .
626 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Определение маршрута Маршрут, посредством которого достигаются контроллеры АР!, может определяться толь­ ко с использ ованием атрибута Ro ute , но не конфигурации приложения в классе Start up. По соглашению для контроллеров API применяется маршрут, предваренный префиксом ap i , за которым следует имя контроллера, так что контроллер Reservat i onCont r oller из листинга 20.11 достигается через URL вида / api / r eserva t i on : [Route( " api / [controller]")J puЬlic cla s s ReservationController Co ntro l ler { Объявление зависимостей Объекты контроллеров API создаются так же, как и объекты обычных контролле ­ ров, а это значит, что они могут объявлять зависимо сти , которые будут распознав ать ­ ся с использованием поставщика служб. Класс Res e r va tionCon trolle r объявляет в конструктор е зависимость от интерфейса I Reposi tory, которая будет распо знавать­ ся для пр едоставления доступа !{ данным в модели: puЫic ReservationController(IRepository repo) repository = r e po ; Определение методов действий Каждый метод действия декорируется атрибутом . указывающим метод НТГР, кото­ рый он принимает, например: [HttpGet] puЫic IEnumeraЬle<Reservation> Get() => r epository. Reser vations ; Атрибут HttpGet входит в набор атрибутов, применяемых для ограничения досту­ па к методам действий запросами, которые имеют специфич е ский м етод или команду НТГР. Полный набор атрибутов представлен в табл. 20.4 . Таблица 20.4 . Атрибуты методов НТТР Имя HttpGet HttpPost Http De let e Ht tpPut HttpPatch HttpHead AcceptVerbs Описание Этот атрибут указывает, что действие может быть вызвано только НТТР-запросами, которые используют команду GET Этот атрибут указывает, что действие может быть вызвано только НТТР-запросами, которые применяют команду PO ST Этот атрибут указывает, что действие может быть вызвано толь ко НТТР-запросами, которые используют команду DELE TE Этот атрибут указывает, что действие может быть вызвано только НТТР-запросами, которые применяют команду PUT Этот атрибут указывает, что действие может быть вызвано только НТТР-запросами, которые используют команду РАТСН Этот атрибут указывает, что действие может быть вызвано только НТТР-запросами, которые применяют команду HEAD Этот атрибут используется для указания множества команд НТТР
Глава 20. Контроллеры API 627 Маршруты могут дополнительно конкретизироваться за счет включения фраг­ мента маршрутизации в качестве аргумента для атрибута метода НТГР: [HttpGet (" { id} ")] puЫic Reservation Get(int id) => repository[id] ; Фрагмент маршрутизации {id} объединяется с маршрутом. определенным в ат­ рибуте Route, который применен к контроллеру, и ограничением, основанным на методе НТГР. В этом случае действие может быть достигнуто путем отправки запроса GET с URL. соответствующим шаблону маршрутизации /api/reservations/ {id}. где сегмент id затем используется для идентификации объекта Reservation, подле­ жащего извлечению. Обратите внимание, что маршруты, генерируемые для контроллера API, не вклю­ чают переменную сегмента {act ion}, т.е. имя метода действия не является частью URL, требующейся для нацеливания на специфический метод. Все действия в контрол­ лере АР! достигаю тся посредством одного и того же базового URL (/api/reservation в рассматриваемом примере), а метод НТГР и дополнительные сегменты применяются для их различения. Определение результатов действий При пр едставлении ре зультатов методы действий для контроллеров АР! не пола­ гаются на объекты ViewResult, поскольку доставка данных не требует каких-ли­ бо представлений. Взамен методы действий контроллера АР! возвращают объекты данных: [H ttpGet] puЫic IEnumeraЫe<Reservation> Get() => repository . Reservations ; Пр иведенное действие возвращает последовательность объектов Reservation и возлагает на инфраструктуру MVC ответственность за их сериализацию в формат, который может быть обработан клиентом. В разделе "Форматирование содержимого" далее в главе этот процесс объясняется более подробно. Настройка результатов, возвращаемых действиями контроллеров API Один из наиболее привлекательных аспектов контроллеров API заключается в том, что вы можете просто возвращать из методов действий объекты С# и позволить инфраструктуре MVC самостоятельно выяснить, что с ними делать. Инфраструктура MVC довольно хорошо с этим справляется . Например , если вы возвратите из метода действия контроллера API значение null , то клиенту будет отправлен ответ 204 - No Content (204 - содержим ое отсутствует) . Но контроллеры API способны использовать также и средства, доступные обычным конт­ роллерам. Это означает, что стандартное поведение можно переопределить, возвращая из методов действий тип IActionResul t, который указывает вид результата для отправ ки. Ниже показана реализация метода действия из примера контроллера, отправляющая ответ 404 - Not Found (404 - не найдено) для запросов, которые не соответствуют каким-либо объе ктам в модели:
628 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Соге MVC [HttpGet( " {id}")] puЬlic IActionResult Get(int id) { Reservation result = repository[id] ; if (result == null) { return NotFound() ; else { return Ok(result) ; Если в хранилище не обнаруживается объект с указанным идентификатором , тогда вызы­ вается метод NotFound () , создающий объект NotFoundResul t, который приводит к отправке клиенту ответа 4 О 4 - Not Found. Если объект в хранилище найден , тогда вы­ зывается метод Ok () , чтобы создать объект Ob j ectResult. Метод Ok () позволяет от­ правлять объект клиенту внутри действия, которое возвращает IActionResul t , как было описано в главе 17 . Потребность в переопределении стандартны х ответов, возвращаемы х действиями контроллеров API, возникает нечасто , но если она все же появляется, то для этого доступен полный диапазон результатов действий. Тестирование контроллера API Существует множество инструментов, которые по мога ют тести ровать АРI­ интерфейсы веб-приложения. В числе хороших примеров можно назвать Fiddler (www . telerik . com/fiddler), представляющий собой автономный инструмент от­ ладки НТГР-запросов , и Swashbuckle (http : / / gi thub . com/domaindr i vendev / Swashbuckle), являющийся пакетом NuGet , который добавляет в прилож ение стра­ ницу сводки с описания ми его операций АР! и позволяет их тестировать . Но самый простой способ удостовериться в работоспособности контроллера АР! пр едусматривает прим енение PowerShell, который облегчает создание НТГР-запросов в командной строке Windows и позволяет сосредоточиться на результатах опера­ ций АР!, не погружая сь в детали. В последующих разделах будет показано, как ис­ пользовать PowerShell для тестирования операций, предо ст авл я емых контроллером Reservation . Для выполнения тестовых ко манд можно о ткрыт ь новое окно PowerShell или работать в окне консоли дисп етче ра пакетов Visual Studio, которое само взаимо­ действует с PowerShell. Тестирование операций GET Чтобы протестиров ать операцию GET , предлагаемую контроллером Reservation, откройте окно PowerShell и наберите такую команду: Invoke - RestMethod http : // localhos t : 7000/api /reservation - Method GET Эта команда применяет командлет PowerShell по имени Invoke - RestMethod для отправки запроса GET на URL вида /api/rese rvati on . Результат разбирается и фор­ матируется для удобства восприятия данных: reservationid clientName lo cation О Alice 1 ВоЬ 2 Joe Board Room Lecture Hall Meeting Room 1
Глава 20 . Контроллеры API 629 Сервер отвечает на зап рос GET представлением JSON объектов Reservat i on , со­ дер жащихся в модели, которое командлет Invok:e - RestMethod выводит в табличном формате. Формат JSON Формат JSON (JavaScript Object Notatioп - система обозначений для объектов JavaScript) стал ста ндартным форматом данных для веб-приложений . Формат JSON популярен бла­ годаря простоте, лаконичности и легкости в работе с ним . Данные JSON особенно легко обрабатывать в коде JavaScript, потому что формат JSON подобен способу выражения ли ­ теральных объектов на языке JavaScript. Современные браузеры включают встроенную под­ держку для генерации и разбора данных JSON , а популярные библиотеки JavaScript, такие как jQuery, будут автоматически преобразовывать в и из формата JSON. Хотя формат JSON эволюционировал из JavaScript, его структура легка в чтении и понима­ нии для разработчиков на языке С# . Вот как выглядит ответ из контроллера API в примере приложения: [{"reservationid": О, "cl ientName" : "Alice", "location": "Board Room" }, {" reservationi d " : 1 , " c lientName ": " ВоЬ ", "location ": " Lecture Ha ll"}, { " reservationid ": 2 , " cl ientName ":" Joe ", " location ":"Meeting Room 1 " }] Здесь описан массив объектов, который обозначается символами [ и J , а каждый объект отмечается символами { и } . Объекты являются коллекциями пар "ключ-значение", где каж­ дый ключ отделяется от своего значения с помощью двоеточия ( : ), а пары разделяются запятыми (, ). Это отдаленно напоминает синтаксис литералов С# , который использовался в классе MernoryReposi tory для определения данных в листинге 20.3: new Lis t <Reservat i on> { new Reservation { ClientName "Alice", Location = "Board Room" } ' new Reservation { ClientName " ВоЬ ", Location "Lecture Hall" }' new Reservation { ClientNarne "Joe ", Location = " Meeting Room 1 " } Однако обратите внимание, что MVC заменяет соглашение С# по записи имен свойств , на­ чиная с заглавной бу квы (например, ClientNarne), соглашением JavaScript (clientNarne начинается со строчной буквы). Несмотря на то что форматы не идентичны, имеющегося подобия вполне достаточно для того , чтобы разработч ик С# мог читать и понимать данные JSON, не прикладывая больших усилий. В большинстве веб-приложений вам не придется погружаться в детали JSON, т.к. всю рутинную работу берет на себя инфраструктура MVC , но вы можете почитать о формате JSON на веб-сайте www . j son . org. Контроллер Reservation пр едоставляет две операции GET. Когда запрос GET от­ правляется на URL вида /api/reservation , возвращается ответ, содержащий все объекты в модели . Чтобы извлечь одиночный объект, его значение Reservationid указывается как финальный сегмент в URL , наприм е р: Invoke - RestMethod http://localhost : 7000/api/reservation/1 -Method GET Эта команда за пр ашивае т объ ект Reserva t ion со значением 1 в свойстве Reservationid и выводит следующий результат:
630 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC reservationid clientName location 1 ВоЬ Lecture Hall Тестирование операции POST Все операции, предоставляемые контроллером АР!, могут быть протестированы с применением PowerShell, хотя формат команд может оказаться слегка неудобным. Вот как выглядит команда, которая отправляет контроллеру АР! запрос POST для со­ здания нового объе1па Reservation в хранилище и записывает полученные данные в ответ: Invoke-RestMethod http://localhost:7000/api/reservation - Method POST -B ody (@ {clientName = "Anne "; location = "M eeti ng Room 4"} 1 ConvertTo-Json) - ContentType " application/json" В приведенной команде с помощью аргумента -Body указывается тело запроса, которое кодируется как JSON. Аргумент -ContentType используется для установки заголовка Content-Type в запросе. Команда даст следующий результат: reservationid clientName location 3 Anne Meeting Room 4 Операция POST применяет значения clientName и location, чтобы создать объект Reservation и возвратить клиенту представление JSON нового объекта, которое вклю­ чает присвоенное ему значение Reservationid. Может показаться, что клиент прос­ то получает значения данных, которые он отправил серверу в запросе. Тем не менее, такой подход гарантирует, что клиент работает с теми же самыми данными, которые использует сервер, и учитывает любое форматирование или трансляции, вьшолняемые сервером в отношении полученных от клиента данных. Чтобы взглянуть на эффект от запроса POST, отправьте еще один запрос GET на URLвида /ap i /reservation: Invo ke-RestMethod http://localhost:7000/api/reservation - Method GET Возвращенные данные отражают добавление нового объекта Reservation: reservationid clientName location О Alice 1 ВоЬ 2 Joe 3 Anne Тестирование операции PUT Board Room Lecture Ha ll Meeting Room 1 Meeting Room 4 Метод PUT применяется для изменения существующих объектов в модели. Значение Reservationid объекта указывается как часть URL запроса, а значения clientName и l ocation предоставляются в теле запроса. Вот команда PowerShell, которая отправляет запрос PUT для модификации объекта Reservation: Invoke -RestMethod http://localhost:7000/api/reservation - Method PUT -Body (@ {reservationid = " l"; clientName = " ВоЬ "; location = " Media Room"} 1 ConvertTo-Js on) - ContentType " app li cat i on/j son "
Глава 20. Контроллеры API 631 Этот з апрос изменяет объ ект Re s e r vation со значением 1 в свойств е Reservat i onid и указывает новое значение для свойства Location . Выполнив ко­ манду , вы увидите следующий ответ, который отражает внесенное изменение: reservationid c l ientName location 1 ВоЬ Medi a Room Чтобы взглянуть на эффект от запроса PO ST , отправьте еще один запрос GE T на URL вида /api/reservation: Invo ke - RestMe t hod http : //localho s t : 7000/api/reservation - Method GET Возвращ е нны е данные содержат добавленный новый объект Res ervation: reser·vationid clientName l ocation ------------- ------- --- --- --- -- оAlice Board Ro om 1 ВоЬ Media Room 2 Joe Meeting Room 1 3 Anne Meeti ng Room 4 Тестирование операции Del.et;e Последний тест заключается в отправке запроса DE LETE, который удал ит объект Reservation из хранилища: Invoke-RestMethod http : //l ocal hos t: 7000/api/reservation/2 - Me thod DEL ETE Действие, которое принимает запросы DE LE TE в контроллере Re s ervat i on , не возвраща ет результатов, поэтому после завершения команды никакие данные не отоб­ ражаются . Чтобы просмотреть эффект от удаления, запросите содержимое хранили­ ща с использованием такой команды: Invoke - RestMethod http : // l oca l hos t: 7000/api/reservation - Method GET Объект Res e rvation со значением 2 в свойстве Rese r vat i on id был удален из хранилища : reservationid c l ientName location ОAlice 1 ВоЬ 3 Anne Board Roo m Media Room Meeting Roo m 4 Использование контроллера API в браузере Определение контроллера API решает проблему открытости в приложении, но ни­ чего не делает в плане решения проблем скорости и эффективности . Для этого по­ надобится обновить часть HTML приложения, чтобы при отправке НТТР-запросов к 1юнтроллеру API с целью выполнения операций над данными она полагалась на JavaScript. В бр аузере асинхронные НТТР-запросы обычно известны как запросыАjах, где Ajax представляет собой аббревиатуру для Asynchronnus JavaScript and ХМL (асинхронный JavaScript и ХМL) . В последние годы формат данных ХМL подрастерял свою популяр­ ность . но наз вание Ajax по-прежнему применяется для ссылки на асинхронны е НТТР­ запросы, даже когда они возвращают данные JSON. Выражаясь более широко , опи-
632 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC санный в настоящем разделе прием является основой одностраничных приложений, где код JavaScript внутри единственной НТМL-страницы используется для извл е чения данных. которые предназначены множеству разделов прилож ения, с динамической генерацией подлежащего отображению содержимого. На заметку! Разработка клиентской стороны является отдельной темой и выходит за рам­ ки настоящей книги . Здесь мы создадим только базовый асинхронный НТТР-запрос без подробных объяснений, просто чтобы дать вам представление о том, как это делается . Детальное объяснение использования JavaScript и jQuery для создания одностранич­ ных приложений, которые потребляют службы из контроллеров API, ищите в моей книге Pro ASP.NET Core MVC Client Development (издательство Apress). Браузеры предоставляют АРl-интерфейс JavaScript для выполнения запросов Ajax, но работать с ним несколько неудобно. к тому же есть отличия в том, как браузеры реализуют некоторые дополнительные средства. Выполнять запросы Ajax проще все­ го с применением библиотеки jQuery, которая является бесконечно полезным инстру ­ ментом для разработ1ш клиентс1юй стороны. В листинге 20.12 показано содержимое файла bower . j son с добавленным пакетом jQuery. Листинг 20.12. Добавление пакета jQuery в файле bower. j son "name ": "asp.net", " private ": true, " dependencies ": { "bootstrap": "З.3.6", "jquery": "2. 2 . 4" На самом деле. поскольку средства Bootstrap зависят от jQuery. инструмент Bower уже установил пакет в папку wwwroot/ lib. Добавление в листинге 20.12 просто де­ лает такую зависимость явной. На заметку! При добавлении пакетов в проект с использованием Bower вы не всегда будете получать ожидаемые номера версий . Диспетчер пакетов NuGet загрузит несколько версий пакетов и организует их бок о бок, чтобы гарантировать предсказуемую работу всех функ­ ций , однако Bower не может это делать, т.к. браузеры не способны иметь дело с разными версиями того же кода во время выполнения. На момент написания главы последней вер­ сией библиотеки jQueгy была 3.0.0, но в листинге 20.12 указана версия 2.2.4, потому что с ней работает Bootstrap 3.3 .6 . Указание jQuery 3.0.0 в файле bower . j son не приведет к обновлению jQuery инструментом Bower, поскольку это вызовет несоответствие заданной версии и требуемой версии Bootstrap . Чтобы задействовать средства, предлагаемые jQuery, создайте папку wwwroot/j s и добавьте в нее файл по имени c lient . j s с содержимым из листинга 20.13.
Глава 20. Контроллеры API 633 Листинг 20.13. Содержимое файла client.js из папки wwwroot/js $(document) . ready(function () { $( " form " ) .submit(function (е) e . preventDefault() ; $ . ajax({ url: " api/reservation ", contentType: " application/json ", method: " POST "' data : JSON.stringify({ clientName : this.elements [" ClientName "] . val ue, location : this.elements[ " Location"] . value })' success : function(data) addTaЬleRow(data) ; } }) }); }); var addTaЬleRow = function (reservation) $ ( " tаЫе tbody" ) . append ( " <t r><td>" + reservation. reservationid + " </td><td> " + reservation . clientName + " </td><td>" + reservation.location + " </ td></tr>"); Когда пользователь отправляет форму в браузере, файл JavaScript создает ответ, кодирует данные формы как JSON и отправляет их серверу с применением НТТР­ за прос а POST. Данные JSON, возвращаемые сервером, автоматически разбирают­ ся jQuery и затем используются для добавления строки в НТМL-таблицу. В листин­ ге 20.14 обновлена компоновка с целью включения элементов script для библиотеки jQuery и файла client . j s. Листинг 20.14 . Добавление ссылок на файлы JavaScript в файле _ Layout. cshtml <html> <head> <meta name= " viewport " content="width=device - width " /> <t itle >RESTful Controllers</title> <link asp-href - include= "l iЬ/bootstrap/dist/css/*.min . css " rel= " stylesheet" /> <script src="l.iЬ/jquery/dist/jquery.js"></script> <script src="js/client.js"></script> </head> <body class= " panel -b ody " > @RenderBody () </body> </html>
634 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Первый элемент script сообщает браузеру о необходимости загрузки библиотеки jQuery, а второй элемент script указывает файл, который будет содержать специ­ альный код. Запустив приложение и создав с помощью НТМL-формы объект Reservation в хранилище приложения, вы не заметите каких-либо визуальных отличий, но если вы просмотрите НТТР-запрос, отправленный браузером, то увидите, что он требует на­ много меньше данных, чем бьmо в синхронной версии приложения. При простом тес­ тировании асинхронный запрос потребовал 480 байтов данных, что составляет около 40% объема, необходимого в случае синхронного запроса. В реальных приложениях улучшения будут более существенными, т.к. размер данных имеет тенденцию быть го­ раздо меньше размера НТМL-документа, который применяется для их отображения. Форматирование содержимого Когда метод действия возвращает объект С# в качестве своего результата, инф­ раструктура МVС обязана выяснить, какой формат данных должен использоваться для кодирования объекта и отправки его клиенту. В настоящем разделе объясняется стандартный процесс, а также влияние на него запроса, отправляемого клиентом, и конфигурации приложения. Для содействия прояснению работы процесса добавьте в папку Contro lle rs файл класса по имени ContentController . cs и определите в нем контроллер, приведенный в листинге 20 . 15. Листинг 20.15. Содержимое файла ContentController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using ApiControllers . Models ; namespace ApiControllers . Controllers [Route ( " api/ [controller] " )] puЫic class ContentController : Controller [HttpGet("string")] puЬlic string GetString() => "Thi s is а string response"; [HttpGet (" obj ect " )] puЫic Reservation GetObject() => new Reservation { Reservationid = 100 , ClientName = " Joe ", Location = " Board Room " }; Для двух действий в контроллере Con ten t указаны статические переменные сегментов как аргументы атрибута HttpGet, что означает возможность достичь их посредством URL вида /api/contro l ler/string и /api/controller/object . Контроллер Content даже близко не следует паттерну REST, но он облегчит понима­ ние , каким образом работает согласование содержимого. Формат содержимого, выбираемый MVC, зависит от четырех факторов: форматов, которые клиент будет принимать, форматов, которые МVС может выпускать, поли­ тики содержимого, указанной действием, и типа, возвращаемого методом действия. Процесс выяснения , каким образом все сочетается друг с другом, может выглядеть
Глава 20 . Контроллеры АР! 635 пугающим , но хорошая новость в том, что стандартная политика прекрасно работает для большинства приложений . Вы должны понимать , что происходит "за кулисами" , только при необходимости внесения изменений или в случае . если результаты полу­ чают ся н е в том формате , который ожидался. Стандартная политика содержимого Отправной точкой является стандартная конфигурация приложения, которая при­ м еня ется . когда ни клиент, ни метод действия не накладыва ет ограничения на форма ­ ты, которы е можно использовать. В такой ситуации исход прост и предсказуем. 1. Если метод действия возвращает объект s t ring , то он отправляется клиенту не модифициров анным, а заголовок Content - Type ответа устанавливает ся в text/plai n. 2. Для всех остальных типов данных, включая прочие простые типы вроде i nt , данные форматируются как JSON, а заголовок Content- Type ответа устанав­ ливается в applicati on/ j son. Строки получают специальную интерпретацию из-за того, что они могут вызвать проблемы при кодировании в JSON. В результате кодирования других простых типов , таких как значение 2 типа int языка С#. получается строка в кавычках, подобная " 2 ". Когда кодируется строка, итогом будет два набора кавычек, так что "He l l o " ста­ новится ""He llo "". Не все клиенты хорошо стравляются с таким кодированием, поэтому надежнее применять формат t e xt / pl ai n и полностью обойти проблему . Проблема возникает редко, потому что лишь немногие приложения отправляют зна­ чения strin g ; гораздо чаще посылаются объекты в формате JSON. Оба исхода мож ­ но просмотреть с использованием PowerShell. Вот команда. которая вызывает метод GetString () ,который возвращает значение st ring : Invoke-WebReque s t http : // l ocal h o s t : 7000/api/content/s tri ng select @{n = ' Content-Type ';e = { $ .Headers ."Content-Type " } }, Content Данная ком анда отправляет запрос GE T на URL вида /api/content/string и об­ рабатыва ет ответ для отображения з аголовка Co n tent -Type и содержимого отв ет а. Совет. Во время применения командлета In v oke-We b Re qu es t может быть получена ошибка, если не была выполнена начальная настройка для lnternet Explorer. Это особенно вероятно на машине Windows 10, где lnternet Explorer заменен браузером Edge. Проблему легко устранить, запустив IE и выбрав требуемую начальную конфигурацию. Ко манда выдает следующий вьmод: Content - Type Con tent text/plain; charset = utf-8 This is а string response Ту же самую команду можно также использовать для отображения результата в формате JSON. изменив лишь запрашиваемый URL: Invoke - WebRe q u es t h ttp : //loca l ho s t :7 000/api/content/object select @{n = ' Content-Type '; e = { $ .Headers."Cont ent -Type" }}, Content
636 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Команда генерирует вывод, сформатированный для ясности, который показывает. что ответ был закодирован в JSON: Content - Type appli cation/json; char set Согласование содержимого Content utf-8 {" rese r vationid": l OO, '' clientName '' : '' Joe '', " locat ion ":"Board Room " } Большинство клиентов будут включать в запрос заголовок Accept , ука з ывающий набор фор матов, которы е они готовы получать в ответе , выр аженный в виде множ ес­ тва типов MIME . Ниже приведен заголовок Accep t, который посылает в з апросах бр а ­ узер Google Chrome: Acce pt : text / html, appl ication/xhtml + xml , a pp licati on/xml ; q = 0.9,image/webp, */*;q =0.8 Заголовок указывает, что Chrome может обраб атывать форматы HTML и XHTML (ХНТМL - это совместимый с ХМL диалект НТМ L), ХМL . а также формат изображений WEBP. Значения q в заголовке задают относительные предпочт ения, где зн ачение 1. О является стандартным. Указание знач ения q, равного О . 9, для application/xml со­ общает серверу о том, что Chrome будет прини м ать данные XML. но пр едпочита ет иметь дело с HTML или XHTML. Последний элем ент, * / * . уведомляет сервер о том, что Chrome будет принимать любой формат, но его значение q указывает наименьшее предпочтени е с реди п е р е численных типов. В итоге это означ ает, что заголово1t Accept . отправляемый браузером Chrome, снабжает сервер следующей информацией. 1. Бр аузер Chrome предпочитает получать данные HTML или ХНТМL либо изобр а­ жения WЕВР. 2. Если эти форматы не доступны, тогда следующим наиболее пр едпочтит ельным форматом является ХМL. 3. Если ни один из предпочтительных форматов не доступен, то Chrome будет при ­ нимать любой формат. Вы мож ете предположить . что для изменения формата з апроса, получ е нного от приложения MVC, достаточно установить заголовок Ac cept , но он не работает подоб­ нь1м образом - точнее он не работает подобным образом из-за того, что требуется определенная подготовка. Для начала вот команд а PowerShell, посылающая GET м е ­ тоду GetObj ect () запрос с заголовком Accept , в котором указано, что кли ент будет принимать только данные ХМL: Invo ke - WebRequest http : //loca l host : 7000/api /cont e nt/object - Headers @{Accept="application/xml"} 1 sel ect @{n = ' Content-Type ';е = { $ .Headers. "Content-Type" }} , Content Ниже показаны результаты, где видно, что сервер отправил ответ applicat ion/ j son: Content - Type a pplication/j son ; chars et Cont ent utf - 8 {" reservationi d": lOO , " c l ientName ":"Joe ", "l ocation ":"Board Room " }
Глава 20. Контроллеры АР! 637 Включение заголовка Accept никак не влияет на формат, хотя сервер отправил клиенту ответ в формате, 1шторый не был указан. Проблема в том, что по умолчанию инфраструктура МVС сконфигурирована для поддержки только формата JSON, поэ­ тому нет других форматов, которые можно было бы применять. Вместо возвращения ошибки инфраструктура MVC посылает данные JSON в надежде на то, что клиент сумеет их обработать, даже при отсутствии такого формата в числе указанных в за­ головке Accept запроса. Конфигурирование сериализатора JSON Для сериализации объектов в формат JSON инфраструктура ASP.NET Core MVC использует популярный сторонний пакет JSON под названием Js on . Net. Стандартная конфигурация подходит большинству проектов, но может быть изменена, если данные JSON необходимо создавать специфичес ким образом . Расширяющий метод AddMvc () . AddJsonOptions () , вызываемый в классе Startup, позволяет получить доступ к объекту MvcJsonOptions, посредством которого конфигурируется пакет Json . Net. Описание доступных конфигура­ ционных параметров ищите по адресу www . newtonsoft.com/j son. Включение форматирования XML Чтобы увидеть согласование содержимого в работе, понадобится предоставить инфраструктуре MVC определенный выбор в форматах, которые она применяет для кодирования данных ответа. Несмотря на то что JSON стал стандартным форматом для веб - прилож ен ий , MVC мож ет также поддерживать 1<одирование данных как ХМL. Добавьте пакет NuG et, содержащий поддержку ХМL, в раздел dependen ci es файла proj ect . j son , как продемонстрировано в листинге 20.16. Листинг 20.16. Добавление пакета форматирования XML в файле project. j son " dependencies ": { " Microsoft.NETCore . App ": "version": "1 .0 .0 ", " type ": " platforrn " }, "M icrosoft . AspNetCore . Diagnostics ": "1. 0 . 0 ", " Microsoft . AspNetCore . Server. II Sinteg ra tion ": "1. О . О", " Microsoft . AspNetCore . Server . Kestre l": " 1 . О . О ", " Microsoft . Extensions . Logging . Console ": " 1 . 0 . 0 ", " Microsoft . AspNe tCore. Mvc": "1.О.О", " Micr osoft .AspNetCore .StaticFiles ": " 1 . 0 . О ", " Microsoft .Asp NetCore .Ra zor .T ools ": }, " version ": "1. 0 . 0 - preview2-final" , " type ": "build" "Мicrosoft.AspNetCore .Mvc. Formatters .Xml": "1. О. 0" }1 Добавление пакета Microsoft. AspNetCore. Mvc . Formatters . Xml предостав­ ляет доступ к расширяющим методам, которые можно использовать для включения форматирования ХМL в файле Startup . cs (листинг 20. 17) .
638 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Совет. Можно создать собственный формат содержимого , унаследовав его от класса Microsoft . AspNetCore. Mvc . Forma tters. OutputFormatter . Это ред к о при­ носит пользу, потому что создание специального формата данных не является удобным способом открытия доступа к данным в приложении, а са м ые распространенные форма­ ты - JSON и XML - уже реализованы. Листинг 20.17 . Включение форматирования XML в файле Startup. cs using Microsoft . AspNetCore . Builder; u sing Microsoft.Extensions . Dependencyin je ct i on ; using ApiControllers . Models; namespace ApiControl lers { puЬl i c class Startup { puЬlic void ConfigureServices(IServiceColl ection services) { services . AddSingleton<IRepository , MemoryRepository>() ; services.AddМvc() . AddXmlDataContractSerializerFormatters(); puЫic void Conf i gure ( IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . UseMvcWithDefaultRoute() ; Когда инфраструктуре MVC доступен только формат JSON , у нее нет никакого вы­ бора кроме кодирования ответов как JSON. Теперь . когда выбор есть, можно пон аблю­ дать за более полной работой процесса согласования содержимого. Совет. В листинге 20.17 применялся расширяющий метод AddXmlDataContract SerializerFormatters (), но м о ж но так же использовать расширяющий ме тод AddXmlSerializerFormatters () , который предоставляет доступ к более старо­ му классу сериализации. Это мо жет быть полезно при необходимости генерации ХМL­ содер жи мого для более старых клиентов .NET . Вот команда PowerShell , которая снова запрашивает данные ХМL: Invoke - Web Request http : //localhost : 7000/api/content/object -H eaders @{Accept = " app li cation/xml " } 1 sel ect @{n = ' Content-Type ';е = { $ .Headers. " Content-Type" }}, Content Запустив такую команду, вы увидите, что теперь с ерв ер возвращает данные ХМL, а не JSON (для краткости атрибуты пространств имен ХМL опущены): Content - Type Content application/xml ; charset=utf- 8 <Reservation> <Cl i entName>Joe</ClientName> <Location>Board Room</Location> <Reservationid>lOO</Reservationid> </Reservation>
Глава 20. Контроллеры API 639 Указание формата данных для действия Систему согласования содержимого можно переопределить и указать формат дан­ ных прямо на методе действия, применяя атрибут Produces (листинг 20.18) . Листинг 20.18. Указание формата данных в файле ContentController.cs using Microsoft . AspNetCore . Mvc; using ApiControll er s . Mode l s ; namespace ApiControllers . Controllers [Route( " api/[controller]")] puЫic class ContentControl l er : Contro ll er [HttpGet( " string " )] puЫic string GetString () => " This is а string response "; [HttpGet( " object ")] [Produces("application/json")] puЫic Reservat ion GetObject() => new Reservation { Reservation id = 100, ClientName = "Jo e ", Location = " Board Room " }; Атрибут Produces - это фильтр, изменяющий тип содержимого объектов Obj ectResul t , которые используются инфраструктурой МVС "за кулисами" для пред­ ставления результатов действий в контроллерах API. В аргументе атрибута указыва­ ется формат, который будет применяться для результата, возвращаемого действием, и допускается также указание дополнительных разрешенных типов. Атрибут P r oduces принудительно устанавливает формат, используемый ответом, который можно про­ смотреть с помощью следующей команды PowerShell: ( Invoke - WebRequest http : //localhost : 7000/api/content/object - Headers @{Accept = " application/xml " }). Headers. " Content - Type " Эта команда отображает значение заголовка Content-Type из ответа на запрос GET к URL вида /api/content/object. Выполнение команды показывает, что при­ меняется JSON, как указано в атрибуте Produces, хотя заголовок Accept запроса определя ет, что должен использоваться ХМL. Получение формата данных из маршрута или строки запроса Заголовок Accept не всегда находится под контролем программиста, пишущего код клиента, особенно если разработка проводится с применением старого браузера либо инструментального набора. В таких ситуациях может быть удобно разрешить указание формата данных ответа через маршрут, используемый для нацеливания на метод действия, или в разделе строки запроса в URL . Первым делом необходимо оп­ ределить в классе Startup сокращенные значения, которые могут применяться для ссылки на форматы в маршруте или в строке запроса . По умолчанию имеется од~-ю отображение, в котором j son используется в качестве со1<ращения для application/ j son. В листинге 20.19 добавляется дополнительное отображение для ХМL.
640 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 20.19. Добавление сокращения формата в файле Startup. cs using Microsof t .AspNetCor e . Builder; using Microsoft . Extensions . Depe nd e n cyinj ection; using ApiControlle r s . Model s ; using Microsoft.Net.Http.Headers; namespace ApiControl l ers { puЬlic cl ass Start up { puЫic voi d Conf i g ureServices (IServiceCollecti on servi ces) services . AddSi n gleton<IRepository , MemoryRe p ository>() ; services . AddMvc() . Ad dXmlDataCont r actSeria l ize r Formatters() .AddМvcOptions(opts => { opts.FormatterMappings.SetмediaTypeМappingForFormat("xml", new MediaTypeHeaderVa1ue("app1ication/xm1")); }); puЫic void Config u re(IApp l icationBuilder арр) { app . UseSta t usCodePages() ; app . UseDeveloperException Page() ; ap p. UseStaticFi l es() ; app . UseMvcW ithDefa u ltRoute() ; Свойство MvcOption s. F o rm atte rMa p p in g s служит для установки и управл е­ ния отображ ениями. В листинге 20. 19 с использованием метода Se t MediaType Mappi n gForFormat () создается новое отображение, так что сокращени е xml бу­ дет ссылаться на формат applicat i o n/ x ml . Дале е понадобится применить атрибут Format Fil ter к методу действия и дополнительно подкорр ектировать маршр ут для действия, чтобы он включал переменную сегмента format (листинг 20.20) . Листинг 20.20. Использование атрибута FormatFilter в файле ContentController . cs usi n g Mi crosoft . AspNetCore . Mvc ; u s ing ApiCon trollers. Models ; names p ace ApiControll ers . Controlle r s [Route( " api/[controller] " ) ] puЬl i c class Conte nt Controller : Cont roller [HttpGet( " stri ng " )] p uЫic string GetS t ring() => "Thi s is а s t ring response "; [HttpGet("object/{format?}")] [FormatFilter] [Produces("application/json", "application/xml")] puЬl i c Reservation Ge t Object () = > new Reservation { ); Reservationi d = 100 , Cl ientName = "Joe ", Location = " Board Room"
Глава 20 . Контроллеры API 641 К методу GetObj ect () бьm применен атрибут Forrn a tFil ter, а маршрут для данно­ го действия модифицирован так , что он включает необя зательный сегмент forrnat . Вы не обя заны использовать атрибут Produ ces в соч етании с атрибутом Forrn atFi l ter, но если дела ете это, тогда будут работать только запросы, у1<азывающие форматы, для которых бьm с1юнфигурирован атрибут Produ ces. З апросы, указывающие форм ат, для которого атрибут Produ ces н е был СI<онфигурирован , получ ат ответ 404 - Not Foun d . Если вы не применяли атрибут Produ ces , то запрос мож ет задавать любой форм ат, который инфраструктура МVС сконфигурировала для использования. Кроме того , к атрибуту P r oduces добавлен формат application/xrnl, чтобы ме­ тод де йствия поддерживал запросы JSON и ХМL. В следующей команде PowerShell формат xrn l ука зывается как часть URL запроса: (Invoke - WebRequest http : //loca l host : 7000/api/content/object/xml) . Headers. " Conten t -Type " Запус1< этой ко манды приводит к отображению типа содержимого ответа: application/xml ; charset = utf - 8 Атрибут ForrnatFil ter ищет переменную с е гмента маршрутизации по имени format, получ а ет содержащее ся в ней сонращенное знач е ние и извлекает ассоции­ рованный формат данных из конфигурации приложения . Зат ем этот формат приме ­ няется для ответа . Если данные маршрутизации отсутствуют, тогда инспентируется строка запроса. Вот команда PowerShell, которая требует использования формата ХМL посредством строк и запрос а : (Invoke - WebRequest http : //l ocal h ost : 7000/api/content/ object?format =xml ) .Headers." Cont ent - Type " Формат, найденный атрибутом FormatF il t er , переопр ед еляет любые форматы, указанны е в заголовке Accept , что п е р ед ает выбор формата в руки разработчика клиентов, даже когда работа производится с инструм ентальными наборами и браузе­ рами , 1юторы е не позволяют устанавливать заголовок Accept. Включение полного согласования содержимого Дл я большинства приложений отправка данных JSON . когда нет других доступных форматов, является практичной политикой, поскольку клиент веб-приложения скорее некорр е ктно установит свой заголовок Accept, ч е м не сумеет обработать JSON. Тем не мен ее , определенным приложениям придется иметь дело с клиентами , которые вы­ зывают проблемы в случае возвращения данных JSON безотносительно к тому , что указано в заголовке Accept . Обеспечение работоспособности согласования содержи­ мого требует внесения в конфигурацию внутри класса Startup двух измен ений , как иллюстриру ется в листинге 20.21 . Листинг 20.21. Включение полного согласования содержимого в файле Startup. cs using Microsoft . As pN etCore . Bu i l de r; using Microsoft .E xtensions .D epe n de n cyinj ection ; using ApiContro l lers . Models ; using Microsoft . Ne t.Ht t p . Headers ; using Microsoft . AspNetCor e . Mvc .Forrna t ter s ; namespace ApiCo n t r ollers { puЫic class Startup { puЬlic vo i d Configu r eSe r vices( I ServiceCo ll ection services) {
642 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC services.AddSingleton<IRepository, MemoryRepository>() ; services . AddMvc() . Ad d XmlDataContractSerializerFormatters() . A d d MvcOptions(opts => { opts . FormatterMappings . SetMediaTypeMappingForFormat( " xml ", new MediaTypeHeaderVa l ue( "appli cation/xml" )) ; opts.RespectBrowserAcceptHeader = true; opts.ReturnHttpNotAcceptaЬle = true; }); puЫic void Configure(IApplicat i onBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app . UseStaticFiles(); app . UseMvcWithDefaultRoute() ; Параметр RespectBrowserAcceptHeader применяется для управления тем. пол­ ностью ли соблюдается заголовок Accept . Параметр ReturnHttpNotAcceptaЬle используется для управления тем, будет ли клиенту отправляться отв ет 406 - Not АссерtаЫе (406 - неприемлемо) . если подходящий форм ат отсугствует. Нужно также удалить атрибут Produces и з метода действия, чтобы процесс согла­ сования содержимого не переопределялся (листинг 20.22). Листинг 20.22 . Удаление атрибута Produces в файле Contentcontroller. cs using Microsoft.AspNetCore . Mvc ; using ApiControllers . Models ; namespace ApiControllers.Controllers [Route( " api/[controller] " ) J puЫic class ContentController : Controller [HttpGet ( " string " )] puЫic string GetString () => "T his is а string response "; [HttpGet( " object/{format?} " )] [ FormatFilter] //[Produces("application/json", "application/xml")] puЫic Reservation GetObject() => new Reservation { }; Reservationid = 100 , ClientName = " Joe ", Location = " Board Room " Ниже показана команда PowerShell, которая отправляет запрос GET на URL вида /ap i /content/object с заголовком Accept , указывающий тип содержимого, кото­ рый приложение не может предоставить: Invoke-WebRequest http://localhost : 7000/ap i /content/object - Headers @{Accept = " application/custom " } В ре зультате запуска этой команды отображается сообщение об ошибке 406, ука­ зывающее клиенту на то , что сервер не смог предоставить затребованный формат.
Глава 20. Контроллеры API 643 Получение разных форматов данных Когда клиент посылает данные контроллеру, скажем, в запросе POST, с помощью атрибута Consumes можно указывать разные методы действий для обработки специ­ фических форматов данных (листинг 20.23). Листинг 20.23. Обработка разных форматов данных в файле ContentController. cs using Microsoft . AspNetCore . Mvc; using ApiControllers . Models ; namespace ApiControllers .C ontrollers [Route("api/[controller]")] puЫic class ContentController : Controller [HttpGet("string")J puЬlic string GetString () => "This is а string response"; [HttpGet( " object/{format?} " )] [FormatFi lter] //[Produces( " application/json ", " application/xml " )] puЫic Reservation GetObject() => new Reservation { }; Reservationid = 100, ClientName = "Joe", Location = "Board Room " [HttpPost] [Consumes("application/json")] puЬlic Reservation ReceiveJson([FromВody] Reservation reservation) { reservation.ClientName = "Json"; return reservation; [HttpPost] [Consumes("application/xml")] puЫic Reservation ReceiveXml([FromВody] Reservation reservation) { reservation.ClientName = "Xml"; return reservation; Действия ReceiveJson и ReceiveXml принимают запросы POST. Отличие между ними связано с форматом данных, указанным в атрибуте Consumes, который иссле­ дует заголовок Content -Type, чтобы выяснить, может ли метод действия обработать запрос. В результате, когда поступает запрос с заголовком Content -Type, установ­ ленным в application/j son, будет применяться метод Recei veJson (),но если за­ головок Content-Type установлен в application/xml , тогда будет использоваться метод Recei veXml () . Резюме В настоящей главе объяснялась роль, которую контроллеры API играют в при­ ложениях МVС. Было продемонстрировано, каким образом создавать и тестировать контроллер АР! и как выполнять НТТР-запросы с применением jQuery. Вдобавок был описан процесс форматирования содержимого. В следующей главе более подробно рассматривается работа представлений и механизмов визуализации.
ГЛАВА 21 Представления в главе 17 было показано. что методы действий могут возвращать объекты ViewResul t, которые сообщают инфраструктуре MVC о необходимости визуа­ лизации представления и возвращения НТМL-ответа клиенту. Вы уже видели исполь­ зование предс тавле ний во многих примерах книги , поэтому должны приблизит ельно понимать, что они делают, но в этой главе мы погрузимся в детали. Мы начнем с того, что покажем , как инфраструктура MVC обрабатывает объекты ViewResul t с примен ени ем механизмов вuзуалuза4uи, и продемонстрируем созда­ ние специального механизма визуализации. Кроме того, мы опишем приемы эффек­ тивной работы со встроенным механизмом визуализации Razor, в том числе использо­ вание частичных представлений и разделов компоновки, которые являются важными темами для разработки эффективных приложений MVC. В табл . 21.1 приведена свод­ ка, позволяющая пом е стить представл ения в контекст. Таблица 21.1 . Помещение представлений в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие­ то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с вер­ сией MVC 5? Ответ Представления - это часть паттерна MVC, применяемая для отобра­ жения содержимого пользователю. В приложении ASP.NET Core MVC представление является файлом, содержащим НТМL-элементы и код С#, который обрабатывается для генерации ответа Представления позволяют отделять презентацию данны х от логики, которая обрабатывает запросы. Представления также дают возмож ­ ность применять ту же самую презентацию повсюду в приложении , т.к. многие контроллеры могут использовать одно и то же представление В большинстве приложений MVC применяется механизм визуализа- ции Razor, который облегчает смешивание содержимого HTML и С#. Представления выбираются за счет возвращения объекта ViewResul t в качестве результата метода действия, как было описано в главе 17 Привыкание к использованию Razor и его смеси HTML и С# может занять какое-то время. В этой главе объясняется работа механизма Razor, что поможет в прояснении ряда его операций Для инфраструктуры MVC доступно несколько сторонних механизмов визуализации , но их применение ограничено Razor остается стандартным механизмом визуализации в MVC , но во внутренние АРl-интерфейсы были внесены некоторые изменения. Самое важное изменение связано с тем, что представления, частичные представления и разделы визуализируются асинхронным образом, чтобы улучшить показатели производительности. Крупнейшим измене­ нием является прекращение поддержки дочерних действий и их замена компонентами представлений (которые рассматриваются в главе 22)
Глава 21. Представления 645 В табл. 21.2 приведена сводка для настоящей главы . Таблица 21.2 . Сводка по главе Задача Решение Листинг Соэдание специального механизма Реализуйте интерфейсы 21.1 -21.6 визуализации I ViewEngine и I Vi ew Определение областей содержимо- Используйте разделы Razor 21.7 -21 .19 го для применения в компоновке Соэдание многократно используе- Применяйте частичные 21.20-21 .23 мых фрагментов разметки представления Добавление содержимого JSON Используйте выражение 21.24-21 .26 в представления @Json. Serial ze Изменение местоположений, в ко - Создайте расширитель местополо- 21.27-21 .31 торых Razor ищет представления жений представлений Подготовка проекта для примера Создайте новый проект типа Empty (Пустой) по имени Vi ews с использованием шаб­ лона ASP .NET Core Web Application (.NET Core) (Веб-приложение ASP.NEТ Core (.NЕТ Core)). Добавьт е пакеты МVС в раздел dependencies файла pro j ect . j son (листинг 21 . 1). Листинг 21.1 . Добавление пакетов в файле project. j son " dependencies ": { " Microsoft . NETCore . App ": "version": "1.0 .0", " type ": " platforrn" }1 }1 "Microsoft .AspNet Core . Diagnostics ": "1 . О. О ", "Microsoft . AspNetCore . Server . IISint egration ": "1. 0 . 0 ", "Microsoft.AspNetCore .Server.Kest rel": " 1 . 0 . О ", "Microsoft.Extensi ons .Logging .Console": "1 .0 .0", "Microsoft. AspNetCore. Mvc" : "1 . О. О" , 11 Microsoft.AspNetCore.StaticFiles": "1.0.0 11 1 "Microsoft.AspNetCore.Razor.Tools": "version": "1.0 .0 -preview2-final", "type": "build" " tools ": "Microsoft .AspNetCore . Server .IISintegrati on. Too l s ": "1. 0 . 0 - pr evi ew2 -final ", "Microsoft.AspNetCore.Razor.Tools": "1.0.0 -preview2-final" }1 " frarneworks ": ( " netcoreappl . O": " irnports ": ["dotnetS . 6 11 1 "portaЫe-net45+win8 "] }'
646 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC " buil dOptions ": { " e rn itEntryPoint ": tr ue , "p reserveCornp i lationContext ": t r ue }1 "run t irneOption s ": { " confi gProperties ": { " System . GC . Server": true } В листинге 21 .2 показан класс Startu p , 1юторый конфигурирует средства. предо­ ставляемые пакетами NuGet . Листинг 21.2. Содержимое файла Startup. cs u s i ng Microsoft . AspNetCore . Bu ilder ; using Microsoft . Extensions . De pendencyinjection ; n amespace Views { puЫic class St artup puЫic void ConfigureServi ces(IServiceCol l ect i on services ) { services . AddМvc(); puЫic void Configure(IAppl icationBu i lder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseмvcWithDefaultRoute(); Создайте папку Cont r ollers, добавьте в нее файл класса по имени HomeController. cs и определит е контролл е р. прив еденный в лис т инг е 21.3 . Листинг 21.З. Содержимое файла HomeController. cs из папки Controllers using System ; u sing Microsoft . AspNetCore.Mvc ; narn e space Vi e ws . Controllers { puЫic class HorneContro l ler : Controlle r puЬlic Vi ewRes ult I ndex() { ViewBag . Message = "Hello, World"; Vi ewBag . Tirne = DateTirne . Now . ToStri ng("HH: mrn:ss " ); return View("DebugDat a"); puЫic ViewResu l t List() => View() ;
Глава 21. Представления 647 Создание специального механизма визуализации Мы собираемся впасть в крайность и создать специальный механизм визуализа­ ции. В большинстве проектов делать это не придется, т.к. инфраструктура MVC со­ держит встроенный механизм визуализации Razor, синтаксис которого был описан в главе 5 и который применялся во всех примерах, рассмотренных до сих пор (и вскоре будет использоваться снова). Ценность создания специального механизма визуализации состоит в том, что вы увидите происходящее "за кулисами" и расширите свои знания работы МVС. Вдобавок вы поймете, насколько большую свободу имеют механизмы визуализации при транс­ ляции объектов ViewRe sul t в ответы, предназначенны е клиентам. Механизмы визуализации - это классы, реализующие интерфейс IViewEngine, который определен в пространстве имен As pNetCore . Mvc . ViewEngines. Вот опре­ деление интерфейса IViewEngine: namespace Microsoft . AspNetCore . Mvc . ViewEngines puЫic interface IViewEngine { ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage); ViewEngineResult FindView(ActionContext context , string viewName , bool isMa inPage) ; Роль механизма визуализации заключается в трансляции запросов к представле­ ниям в объекты ViewEngineResult. Когда инфраструктура MVC нуждается в пред­ ставлении, она начинает с вызова метода GetView (),который дает механизму визу­ ализации возможность предоставить представление, просто используя его имя. Если методу GetView () не удалось предоставить представление, тогда вызывает­ ся метод FindView (),так что механизм визуализации получ ает шанс поиск ать пр ед­ ставление с применением объекта ActionContext, который содержит информацию о методе действия, создавшем объект ViewResu l t . Работа механизма визуализации связана со снабжением инфраструктуры MVC объектами ViewEngineResult , которые могут использоваться для ген ерации ответов. Создавать экземпля ры класса ViewEngineResul t н апр ямую нельзя, но для создания э1,земпляров он предлагает статические методы, которые перечислены в табл. 21 .3 . Таблица 21.З. Статические методы класса ViewEngineResul t Имя Found (name , v i ew) NotFound(name , locations) Описание Вызов этого метода предоставляет инфраструктуре MVC запрошенное представление, которое указывается в пара­ метре view . Представления реализуют интерфейс IView Вызов этого метода создает объект ViewEngineResul t , который сообщает инфраструктуре MVC о том , что за­ прошенное представление не удалось найти. Параметр l ocat i ons - это перечисление значений string, описы­ вающих местоположения, в которых механизм визуализа­ ции искал представление
648 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC При написании механизма визуализации выбирается один из методов, описан­ ных в табл. 21 .3, для указания исхода запроса к представлению. Метод Found () создает объект ViewEngineResul t, который обозначает успешный запрос и пре­ доставляет MVC представление для обработки. Метод NotFound () создает объект ViewEngineResu l t, который отражает неудачный запрос и снабжает инфраструк­ туру MVC списком местоположений, просмотренных механизмом визуализации во время поиска представления (они будут отображаться разработчику как часть сооб­ щения об ошибке). Еще одним строительным блоком системы механизма визуализации является ин­ терфейс IView , который применяется для описания функциональности, обеспечи­ ваемой представлениями вне зависимости от того, какой механизм визуализации их создал. Интерфейс I View определен следующим образом: using Microsoft . AspNetCore . Mvc .Rendering ; using System.Threading .T asks ; namespace Microsoft.AspNetCore . Mvc . ViewEngines puЫic interface IView ( string Path { get ; ) Task RenderAsync(ViewContext context) ; Свойство Ра t h возвращает путь к представлению, что предполагает определение представлений как файлов на диске. Метод RenderAsync () вызывается инфраструк­ турой MVC для генерации ответа клиенту. Данные контекста пер едаются представле­ нию через экземпляр класса ViewContext, который является производным от класса ActionContext. В дополнение к свойствам контекста, унаследованным от родитель­ ского класса (которые обеспечивают доступ к запросу, данным маршрутизации, кон­ троллеру и т.д.), класс ViewContext предлагает свойства, удобные для использова­ ния при визуализации ответов: наиболее полезные свойства такого рода приведены в табл. 21.4 . Таблица 21.4 . Полезные свойства класса ViewContext Имя ViewData TempData Writer Описание Это свойство возвращает объект ViewDataDictionary, который содержит данные представления, предоставленные контроллером Это свойство возвращает словарь, содержащий временные данные (как описано в главе 17) Это свойство возвращает объект TextWri ter , который должен применяться для записи вывода из представления Самым интересным из всех перечисленных является свойство ViewData, воз­ вращающее объект ViewDataDictionary . В классе ViewDataDictionary определе­ но несколько полезных свойств, которые предоставляют доступ к модели представ­ ления, данным ViewBag и метаданным модели представления. В табл. 21.5 описаны наиболее полезные из этих свойств.
Глава 21 . Представления 649 Таблица 21.5 . Полезные свойства класса ViewDataDictionary Имя Model ModelMetadata ModelState Keys Описание Это свойство типа object возвращает данные модели, предостав­ ленные контроллером Это свойство возвращает объект ModelMetadata, который может использоваться для рефлексии типа данных модели Это свойство возвращает состояние модели, которое рассматри­ вается в главе 27 Это свойство возвращает перечисление значений ключей, которые могут применяться для доступа к данным ViewBag Простейший способ посмотреть, как все это работает - IViewEngine, ViewEngineResul t и IView - предусматривает создание механизма визуализации. Мы построим простой механизм визуализации, который возвращает один вид пред­ ставления. Представление будет визуализировать результат, содержащий информа­ цию о запросе и данные представления, которые сгенерированы методом действия. Такой подход позволяет продемонстрировать способ оперирования механизмов визу­ ализации, не увязая в разбор е шаблонов представлений и не занимаясь воссозданием других средств. которые поддерживает Razor. Создание специальной реализации интерфейса IView Начнем с создания реализации интерфейса IView . Добавьте в проект папку Infrastructure и создайте в ней файл класса по имени DebugDataView. cs, содер­ жимое которого показано в листинге 21.4 . Листинг 21.4. Содержимое файла DebugDataView. cs из папки Infrastructure using System ; using System . Text; using System.Threading.Tasks; using Microsoft.AspNetCore . Mvc . Rendering ; using Microso ft.AspNetCore.Mvc.ViewEngines; namespace Views . Infrastructure { puЫi c class DebugDataView : IView { puЫic str ing Path => String .E mpty ; puЫic async Task RenderAsync(Vi ewContext context) { context .HttpContext . Response . ContentType = "text /plain"; StringBui lder sb = new StringBui lder() ; sb . AppendLine(" --- Routing Data---"); //Данные маршрутизации foreach (var kvp in context . RouteData . Values) { sb . AppendLi ne( $ " Key: {kvp . Key} , Value: {kvp.Value} " ) ; sb . AppendLi ne("---View Data- --" ) ; //Данные представления foreach (var kvp in context .ViewData) { sb . AppendLine($ "K ey : {kv p.Key}, Value: {kvp.Value} "); await context.Writer . WriteAsync(sb .T oString() ) ;
650 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Когда это представление визуализируется, оно записывает детали данных мар­ шрутизации и данных представления, полученные с использовани е м аргумента ViewCont e x t метода RenderAs ync () . Ответом является простой текст, поэтому с помощью объекта контекста заголовок Cont en t -T y p e запроса устанавлив ается в text/pla in. Без такого присваивания ASP.NET по умолчанию применит t e xt/ht ml , что заставит браузер отображать данные в виде единственной неразбитой строки символов. Создание реализации интерфейса rviewEngine Цель механизма визуализации -выпуск объекта Vi ewEngineResult , который со­ держит либо экземпляр реализации I View, либо список мест, где производился поиск подходящего представления. Теперь, имея рабочую реализацию интерфейса IV i ew, можно создать механизм ви зуализации. Доб авьте в папку Infrastructure файл класса по имени Debu gDa t aV i ew Eng ine . cs с содержимым из листинга 21.5 . Листинг 21.5 . Содержимое файла DebugDataViewEngine. cs из папки Infrastructure using Mi crosoft. AspNetCore . Mv c ; using Microsoft . AspNetCore . Mvc.ViewEngines ; namespace Views . Infrastruct u r e { puЫic class Debug DataViewEn gi n e : I ViewEng ine { puЬlic ViewEngineRe sult GetV i ew(string executingFi lePath , string vi e wPath , bool i sMainPage) { ret u rn ViewE ngineResu l t. NotFound( v iew Path , new string[] { "(Debug View Engine - GetView) " }) ; puЬlic ViewEngineResult FindV i ew(Acti onCo nt e xt context , string v i ewNa me , bool isMai nP age) { if (vi ewName == "D ebugData " ) { return Vi e wEngineResult . Found(viewName , new DebugDataView()) ; else { return ViewEngineResult . NotFound(vi ewName , new stri ng [] { "(Debug Vi ew Engine - FindView) " }) ; М етод GetView () в этом механизме визуали з ации всегда во з вр аща е т ответ No t Found. Метод FindV i ew () поддерживает только одно представление, которое на­ зывается Deb u gDa ta. Когда м еханизм визуализации получает запрос представ ления с таким имен ем. он возвращает новый экземпляр класса DebugDataView: if (viewName == "DebugData" ) { return ViewEngine Resu l t .Found(viewNa me, new De b ugDa t aView( )); При реализации более сов е ршенного механизма визуализации эта возможность использовалась бы для поиска шаблонов. В рассматрив аемом простом примере требу­ ется только создани е нового экземпляра класса DebugDataView. В случае получения запроса представлен и я, отличного от DebugData, создается ответ NotFound:
Глава 21 . Представления 651 return ViewEngineRes ul t . NotFo und(vi ewName , new stri ng[] { "(Debug View Engine - FindView) " }); Метод ViewEngineRes ul t. NotFound () предполагает, что механизм визуализа­ ции располагает местами, в которых необходимо искать представления. Это обос­ нованное предположение, поскольку представления обычно являются шаблонами, хранящимися как файлы проекта. В данном случае искать негде, потому просто воз­ вращается фm<тивное местоположение, которое укажет, какой метод был вызван для поиска представления. Регистрация специального механизма визуализации Механизмы визуализации регистрируются в классе Startup путем конфигуриро­ вания объекта MvcViewOptions , как показано в листинге 21.6 . Листинг 21.6. Регистрация специального механизма визуализации в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions . Dependencyinjection ; using Microsoft . AspNetCore . Mvc ; using Views.Infrastructure; namespace Views { puЬlic class Startup puЫic void ConfigureServices(IServiceCollection services) services . AddMvc() ; services.Configure<МvcViewOptions>(options => { options.ViewEngines.Insert(O, new DebugDataViewEngine()); }); puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app .U seMvcWi th DefaultRoute() ; В классе MvcviewOptions опр едел ено свойство viewEngines , которое предст ав­ ляет собой коллекцию объектов реализации IViewEngine. Механизм Razor добавля­ ется в коллекцию ViewEng i ne с помощью метода AddMvc () , и стандартный меха­ ни зм визуализации дополняется специальным классом. Когда инфраструктура MVC получает объект Vi ewResult от метода действия, она вызывает метод FindView () каждого механизма визуализации, содержащего­ ся в коллекции MvcviewOptions. ViewEngines, до тех пор пока не получит объект viewEngineResul t. который был создан с применением метода Found () . Порядок, в котором механизмы добавляются в коллекцию MvcViewOptions . ViewEngine s, важен в случае, если два или большее число механизмов способны об­ служивать запрос к представлению с одним и тем же именем. Чтобы специальный ме-
652 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC ханизм визуализации имел приоритет, он должен быть вставлен в начало коллекции ViewEngine s, как это делалось в листинге 21.6 . Тестирование механизма визуализации Когда приложение запускается, браузер автоматически переходит на корн евой URL проекта, который будет отображен на действие I n dex контроллера Home. Этот метод действия использует метод View () для возвращения объекта Vi ewRes u lt, ко­ торый указывает представление De b ugDa ta. Инфраструктура МVС обратится к коллекции механизмов визуализации и н ачн ет вызывать их методы FindView () . Поскольку запрошенное представление является именно тем, на обработку которого настроен специальный механизм визуализации, он снабж ает МVС представлени ем, отображающим результаты, как показано на рис . 21 .1 . [j localhost55775 ,~~- ~ @>\;~st:.55775 1 ---Routiпg Data--- 1 кеу: cont roller, value: ноте Кеу: action, val ue : Index - -- Vi e1<1 Data--- кey : Message, Value : Hel l o, world кеу: Time, Value: 16:09:05 [___ _______! Рис. 21.1. Использование специального механизма визуализации Чтобы посмотреть , что произойдет, когда ни один из механизмов визуализации не может предоставить представление , запросите URL вида / Home / Li st . В таком слу ­ чае будет создан объект Vi ewResul t , указывающий представление по имени List, которое не способен предоставить ни Razor, ни специальный механизм визуализации. Отобразится сообщение об ошибке, продемонстрированное на рис. 21.2 . С\ lntt-rNI Server Erro1 Х 1:nun:~~~~~~~::~~:cu~~-w~~~~~=~n~~I request. lnvalidOperat ionExceprion: Th e vie1v 'L ist' was oor found . Th e follo1ving locations wеге searcl1ed: {Debug Viev.1 Engiпe - GetV ievv) (DebLJ9 Vievv E ngiпe - FiпdViev1) Nievvs7 Home/ List .cs 11tml /Vie1vs/Sl1a1·ed/List .csl1tml Mkrosoft.AspNl?tCore.M vc. Vie~v En gi ne s.V ie\ч Engin eR esu l t.En sureSucce ss1u l (IE11un1erab l e· ·1 ~1nallocations) Рис. 21.2. Запрашивание представления, которое не может быть предоставлено
Глава 21. Представления 653 Здесь видно , что сообщения, выдаваемые специальным механизмом визуали за­ ции , присутствуют в списке местоположений, в которых выполнялся поиск представ­ ления List , наряду с местоположениями, проверенными механизмом Razor. Когд а нужно гарантировать , что будет применяться только специальный мех а­ низм визуализации, понадобится вызвать метод Cl ear () на коллекции механизмов визуализации , чтобы удалить из нее Razor (листинг 21.7). Листинг 21.7. Удаление других механизмов визуализации в файле Startup. cs using Microsoft . As pN e tCore . Bui l der ; using Microsoft . Extens i ons . Depend encyin j ection ; using Microsoft . As pNe t Core . Mvc ; using Views.Infra s t r ucture ; namespace Vi ews { puЫic class Star t u p puЫic void ConfigureServices (IServiceCollecti on servi ces) services . AddM v c() ; services . Con f ig u re<MvcViewOptions >(options = > { options.ViewEngines.Clear(); options . Vi ew E ng i nes .I nse r t(O , new DebugDataV i ewEngine() ) ; }); puЬlic v oid Configure(IAppl icationBui lde r арр) { app . UseSta t usCodePages() ; app . UseDeve l o p erExceptionPage() ; app . OseStati c Files() ; app.OseMvcWithDefaultRoute(); Если запустить приложение и снова перейти на URL вида / Home/L i st , то легко заметить , что используется только специальный механизм визуализации (рис . 21.3). D lnternat Server Error Х f- С ГФio:~~l10~;557~5/Ho~e(!::st -··---------~-==--- ~ 1-;n·-~nha-;;dled·~·xce~b-on o~curr~d whil~ ~;~~~ssing ~h-;--::- 1 request . · 1 lпvaliclOperat i onException: The vie\.Y 'List' 1.vas поt foL1пd. Tl1e follo~vi11g 1 l ocat i oпs v1ere sea rched: 1 (Debug View Engi11e - GetV ievv>) 1 . (Debug V iev{ Е пgiпе - FiпdVie\N) 1 M icrosolt.AspNetCore.Mvc.Vie1\•Engil\es .Vie1•1EngineResul t.E nsureSuccesslul(I En un1eraЬle ' 1 originallocations) Онеrу Cookies ,_ . ___ _.____ __ . Heade1s J w~"'·' . "- . . ~ ----·----- --------------- . Рис. 21.3 . Применение только специального механизма визуализации в примере приложения
654 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Работа с механизмом Razor В предыдущем разделе у нас была возможность создать специальный механизм визуализации за счет реализации всего лишь двух интерфейсов. Правда. итогом ста­ ло простое подобие механизма, который генерировал неуклюжее содержимое, но зато вы увидели, насколько легко инфраструктура МVС позволяет добавлять или заменять основную функциональность. Сложность в механизм визуализации привносится системой шаблонов представ­ лений, которая включает фрагменты кода, поддерживает компоновки и обеспечива­ ет оптимизацию производительности. В своем простом механизме визуализации мы ничего подобного не делали (и по большому счету не должны), потому что встроен­ ный механизм Razor предоставляет все эти средства и многое другое. На самом деле в Razor доступна функциональность, которая требуется практически во всех приложе­ ниях МVС. Лишь ничтожно малое количество проектов берут на себя труд создавать специальный механизм визуализации. Основы синтаксиса Razor приводились в главе 5, а в настоящем разделе будет по­ казано, как использовать другие средства для создания и визуализации представле­ ний Razor. Вы также научитесь настраивать механизм Razor. Подготовка проекта для примера Для подготовки проекта к работе с Razor потребуется внести ряд изменений . Модифицируйте действие Index контроллера Ноте, чтобы оно выбирало стандартное представление и предоставляло некоторые данные модели (листинг 21.8). Листинг 21.8. Изменение действия Index в файле HomeController. cs using System; using Microsoft . AspNetCore . Mvc; namespace Views . Controllers { puЫic class HomeController : Controller puЫic ViewResul t Index () => View(new string[] { "Apple", "Orange", "Pear" }) ; puЫic ViewResult List() => View() ; Для снабжения метода действия Index () представлением создайте папку Views/ Ноте и добавьте в нее файл по имени Index . cshtml с содержимым, приведенным в листинге 21.9. Листинг 21.9 . Содержимое файла Index. cshtml из папки Views/Horne @model string [] @{ Layout = null; < !DOCT YPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Razor</title> <link asp-href- include= 11 liЬ/boots trap/d ist/css/* . min. css 11 rel= 11 stylesheet 11 /> </head>
<body class= " panel - body " > This is а list of fruit narnes: @foreach (str i ng narne in Model) <span><b>@narne</b></span> </body> </htrnl> Глава 21. Представления 655 Представление полагается на СSS-библиотеку Bo otstrap. Чтобы доб авить Boots trap в про ект, создайте в корневой папке проекта ф айл bower . j son с применением шаб­ лон а элемента Bower Configuration File (Файл конфигурации Bower) и добавьте пакет Boo t strap в е го р а здел dependencies (лист инг 20.10). Листинг 21.1 О. Содержимое файла bower. j son "name ": "asp . net ", " private ": true , " dependencies ": ( "bootstrap": "3. 3. 6 11 Со здайте в п апке Views файл _ Viewimports . cshtml с выр аже нием, показан­ ным в л истинге 21. 11, для включ ения встро е нных дескрипторны х вспо м огательных классов. Листинг 21.11. Содержимое файла_Viewimports. cshtml из па п к и Views @addTagHelper * , Microsoft . AspNe t Core . Mvc . TagHelpers Финальный подготовительный шаг связан с переустановкой механизмов визуали­ зации в клас с е Startup с целью удаления специального м еханиз ма и удаления вы з о­ ва метода Clear (), который отключает Razor (листинг 21 . 12). Листинг 21.12. Переустано в ка механиз мов визуализации в файле Startup. cs using Microsoft . Asp NetCo r e . Bu i lder; using Microsoft . Extensions . Dependencyi njection ; using Microsoft . AspNetCo r e.Mvc ; using Views .I nfras t ructu r e ; narnespace Views ( puЫic class Startup puЫic void ConfigureServices(IServiceCol lection services) services . AddMvc() ; // services.Configure<МvcViewOptions>(options => { // options.ViewEngines.Clear(); / / options. ViewEngine s. Insert (О, new DebugDataViewEngine () ) ; //}); puЫic void Configure(IApplicationBuilder арр) {
656 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC app . UseStatusCodePages(); app.UseDeveloperExceptionPage() ; app .U seStaticFiles(); app . UseMvcWithDefaultRoute(); Запустив приложение, вы увидите результат, представленный на рис. 21.4 . Рис. 21.4 . Запуск примера приложения Прояснение представлений Razor Даже начальное понимание работы механизма Razor может помочь поместить большой объем функциональности в контекст и приоткрыть тайну обработки файлов CSHTML. Так каким же образом Razor берет смесь элементов HTML с операторами С# и вы­ пускает содержимое для НТТР-ответа? Ответ довольно прост и основывается на фун­ кциональности МVС, о которой вы уже узнали в предшествующих главах. Механизм Razor преобразует файлы CSHTML в классы С#, компилирует их и затем создает но­ вые экземпляры каждый раз, когда представлению требуется сгенерировать резуль­ тат. Вот класс С# , который Razor создает для представления Index. csh trnl из лис­ тинга 21.12: using System.Thread ing.Tasks; using Microsoft . AspNetCore . Mvc; using Microsoft.AspNetCore.Mvc . Razor ; using Microsoft.AspNetCore.Mvc.Razor .Internal ; using Microsoft.AspNetCore.Mvc.Rendering; namespace Asp { puЫi c class ASPV_Views_Horne Index_cshtml : RazorPage<string(]> puЫic IUrlHelper Url { get; private set ; } puЬlic IViewComponentHelper Component { get ; private set; } puЬlic IJsonHelper Json { get; private set; } puЬlic IHtmlHelper<string(]> Html { get; private set; } puЫic override async Task ExecuteAsync() { Layout = null; WriteLiteral(@"<!DOCTYPE html> <html ><head> <meta name=" " viewport" " content= "" width=device - width "" />
Глава 21. Представления 657 <title>Razor< /ti t l e > <link asp-href-include=""liЬ/bootstrap/dist/css/*.min.css"" rel=""stylesheet"" /> </head><body clas s=""panel-body"">This i s а list of fruit names:" ); foreach (st ri ng name in Model) WriteLit eral("<span><b>"); Wri te(name) ; Wri teLi teral("</b></span>" ) ; } Wr iteLiteral("</body>< /ht ml >" ) ; Код этого класса бьm приведен в порядок, чтобы облегчить его чтение. Кроме того, были удалены операторы С#, которые Razor добавляет с целью оснащения инстру­ ментами при генерации класса. В последующих разделах этот класс разбивается на части, и приводятся объяснения работы скомпилированных представлений. На за метку! Прежде было легко просматривать классы, созданные более ранними версиями Razor, т.к. для каждого представления генерировался дисковый файл С#, который затем компилировался и использовался в приложении. Изучение класса сводилось просто к на­ хождению правильного файла. Текущая версия Razor опирается на усовершенствования компилятора С# , которые позволяют коду генерироваться и компилироваться в памяти, что обеспечивает улучшение производительности, но затрудняет выяснение происходя­ щего. Чтобы получить показанный выше класс, пришлось изменить предназначение ряда модульных тестов, входящих в состав исходного кода ASP.NET Core MVC, которые предо­ ставили фиктивные реализации классов, применяемые Razor для нахождения и обработки файлов представлений. Это не то, что вам придется предпринимать при повседневной разработке, но такой процесс позволяет выявить очень многие детали о том, как работа­ ют представления . Имя класса Лучше всего начать с имени класса, который создается Razor: p uЫi c c l ass ASPV_Views_Home_Index_cshtml : Razo rP age<string[ J> Механизму Razor нужен какой-нибудь способ для трансляции имени и пути к фай­ лу CSHTML в кл асс, который он создает, когда производит разбор файла, и он делает это за счет кодирования информации в имени к11асса. Механизм Razor снабжает имя класса префиксом AS PV, за которым следует имя проекта, имя контроллера и, нако­ нец, имя файла представления: такая комбинация облегчает проверку доступности класса при запрашивании инфраструктурой МVС представления через интерфейс I Vi ewEng ine , описанный ранее в главе. Базовый класс Многие основны е средства Razor, такие как ссылка на модель представления в виде @Model , возможны благодаря базовому классу, производными от которого явля ­ ются сгенерированные классы:
658 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC puЫic class ASPV_Views_Home Ind ex cshtml : RazorPage<string[]> { Классы представлений наследуются от масса RazorPage или м асса RazorPage<T>, если с помощью директивы @model был ука з ан тип м од ели . Кл а сс RazorPage пр ед­ лаг ае т м етоды и с войс т ва, которые могут исполь з оваться в файл ах CSHT ML для до­ ступа к средствам MVC ; наиболее поле зные методы и свой ства масса RazorPage<T> описаны в табл. 21 .6 . Таблица 21.6. Методы и свойства класса RazorPage<T>, полезные для разработки представлений Имя Model ViewData ViewContext Layout ViewBag TempData Context User RenderSection () RenderBody ( ) IsSectionDefined() Описание Это свойство возвращает данные модели, предоставляемые методом действия Это свойство возвращает объект ViewDataDictionary, который обеспечивает доступ к другим средствам данных представления Это свойство возвращает объект ViewContext, описанный в табл. 21.4 Это свойство применяется для указания компоновки, как было показано в главе 5 и снова упоминается в разделе "Использование разделов компоновки " далее в настоящей главе Это свойство предоставляет доступ к объекту ViewBag , кото­ рый был описан в главе 17 Это свойство предоставляет доступ к объекту TempData, кото­ рый рассматривался в главе 17 Это свойство возвращает объект HttpContext, который опи ­ сывает текущий запрос и подготавливаемый ответ Это свойство возвращает профиль пользователя, ассоцииро­ ванного с текущим запросом. Аутентификация и авторизация пользователей подробно рассматриваются в главе 28 Этот м етод применяется для вставки раздела содер жимо - го из представления в компоновку, как описано в разделе " Использование разделов компоновки " далее в главе Этот метод вставляет в компоновку все содержимое представ­ ления , которое не содержится внутри раздела. Подробности ищите в разделе "Использование разделов к омпоновки" далее в главе Этот метод используется для выяснения, определен ли раздел в представлении Механизм Razor такж е предлагает ряд вспомогательных свойств , которые мож­ но прим енять в представлениях для генерации содержимого (табл. 21 .7).
Глава 21. Представления 659 Таблица 21. 7 . Вспомогательные свойства Razor Имя Описание HtmlEncoder Это свойство возвращает объект HtmlEncoder , который может использо­ ваться для безопасного кодирования НТМL-содержимого в представлении Component Это свойство возвращает вспомогательный объект для компонента пред­ ставления (глава 22) Json Это свойство возвращает вспомогательный объект JSON, как описано в разделе "Добавление содержимого JSON в представления" далее в главе Url Это свойство возвращает вспомогательный объект URL, который может применяться для генерации URL, используя конфигурацию маршрутизации (глава 16) Html Это свойство возвращает вспомогательный объект HTML, который может применяться для генерации динамического содержимого. Данное средство было почти полностью замещено дескрипторными вспомогательными клас­ сами, но все еще используется для частичных представлений , как объясня ­ ется в разделе "Использование частичных представлений " далее в главе Методы и свойства, перечисленные в табл. 21.6 и 21.7, вы будете применять при ежедневной разработке приложений МVС для доступа к данным модели, конфигури­ рования представлений и решения других важных задач. Они приподнимают занавес над использованием механизма Razor, возвращая его в хорошо понимаемый мир С#. Например, когда вы получаете доступ к объекту модели представления либо извлека­ ете значение из TempData посредством @TempData, то ссылаетесь на свойства, кото­ рые определены в классе RazorPage. Визуализация представлений В дополнение к свойствам и методам, предоставляющим средства для разработчи ­ ков, в обязанности класса RazorPage также входит генерирование содержимого от­ ветов через его метод ExecuteAsync () .Этот метод показывает, каким образом Razor обрабатывает файл Index. cshtml с помощью набора операторов С#: puЫic override async Task ExecuteAsync() { Layout = null ; Wr iteLiteral(@"<!DOCTYPE html><html><head> <meta name= "" viewport "" content=""width=device - width "" /> <title>Razor</title> <link asp - href -i nclude= ""liЬ/b ootstrap/dist/css/* .min. css "" rel= "" stylesheet"" /> </head><body class= "" panel - body "" >This is а list of fruit names :" ); foreach (string name in Model) { WriteLiteral( " <span><b>"); Write(name) ; WriteLiteral( " </b></span> "); WriteLiteral( " </body></html> " ) ;
660 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Значения данных, та~ше как значения свойства Model, отправляются клиенту с применением метода Wri te (), который защищает строки так, что они не будут ин­ терпретироваться браузером как НТМL-элементы. Этот аспект важен. поскольку пре­ дотвращает попадание в вывод приложения злонамеренных данных из добавленного содержимого. Метод Wr i teLi teral () не защищает строки и используется для ста­ тического содержимого в файле Index. cshtml, которое, несомненно, браузер должен интерпретировать как НТМL-элементы. В результате статическое и динамическое со ­ держимое файла CSHTML содержится в обычном 1шассе С# и выпускается посредс ­ твом простого вызова метода. Добавление динамического содержимого к представлению Razor Основная цель представлений - сделать возможной визуализацию частей модели предметной области пользователям. Для этого не обходимо уметь добавлять к пред­ ставлениям динамическое содержимое. Динамическое содержимое генерируется во время выполнения и может быть разным для каждого запроса. Оно является проти­ воположностью статического содер;щuмого , такого как НТМL-разм етка , которое со­ здается при написании кода приложения и одинаково для всех запросов. Добавлять динамическое содержимое к представлениям можно различными способами, которые описаны в табл. 21.8 . Таблица 21.8. Добавление динамического содержимого к представлению Прием Встраиваемый код Когда применяется Используется для небольших и самодостаточных порций логики представления, таких как операторы if и foreach. Это фундамен­ тальный инструмент для создания динамического содержимого в представлениях, на основе которого построены другие подходы . Данный прием был описан в главе 5 и затем применялся в много­ численных примерах Дескрипторные вспо- Используются для генерации атрибутов в НТМL-элементах. Дескрип- могательные классы торные вспомогательные классы рассматриваются в главах 23-25 Разделы Применяются для создания разделов содержимого, которые будут вставляться в специфические места внутри компоновки, как объяс­ няется далее в главе Частичные Применяются для совместного использования подразделов ком- представления поновки представления несколькими представлениями. Частичные представления могут содержать встраиваемый код, дескрипторные вспомогательные классы и ссылки на другие частичные представле­ ния. Частичные представления не вызывают метод действия, поэто­ му не могут применяться для выполнения бизнес-логики. Частичные представления описаны далее в разделе Компоненты представлений Применяются для создания многократно используемых элементов управления или виджетов пользовательского интерфейса, которые должны содержать бизнес-логику. Компоненты представлений рас­ сматриваются в главе 22
Глава 21. Представления 661 Использование разделов компоновки Механизм визуализации Razor поддерживает концепцию разделов, которые поз­ воляют организовывать области содержимого внутри компоновки. Разделы Razor обеспечивают больший контроль над тем, какие части представления вставляются в компоновку и куда они помещаются. Чтобы взглянуть на работу разделов, приведите содержимое файла /Views /Home / I ndex . cshtml в соответствие с листингом 21.13 . Листинг 21.1 З. Определение разделов в файле Index. cshtml @mode l string [] @{ Layout = "_Layout"; @section Header { <div class="bg-success"> @foreach (stringstrinnew []{"Ноте", "List", "Edit"}) { <а class="Ьtn Ьtn-srn Ьtn-prirnary" asp-action="str">@str</a> } </div> } This is а list of fruit names: @foreach (string name i n Model ) <span><b>@name</ b ></ s pan> @section Footer { } <div class="bg-success"> This is the footer </div> Из представления были удалены некоторые НТМL-элементы и установлено свойс­ тво Layo u t для указания на то, что при визуализации содержимого должен приме­ няться файл компоновки по имени _ La y o u t . csht ml. Кроме того, в представление были добавлены разделы. Разделы определяются с использованием Rаzоr-выражения @section, за которым следует имя раздела. В лис­ тинге 21.13 созданы разделы с именами He ader и Foo te r. Раздел содержит обычную смесь разметки HTML и выражений Razor, которую можно встретить за рамками раз­ делов в друrих примерах. Разделы определяются в представлении, но применяются в компоновке с помощью . выражения @Rende r S e ct i on . Чтобы посмотреть, как это работает, создайте папку Vi e ws/Shared и добавьте в нее файл компоновки по имени _ Layout . c s h tml, содер­ жимое которого показано в листинге 21.14 . Листинг 21.14 . Содержимое файла _Layout. cshtml из папки Views/Shared <!DOCTYPE html> <h tml> <head> <meta name= " viewport " content= "width=devic e-wi d t h " / > <title>@ViewBag . Tit l e</tit l e> <link asp -href-inc lude= " liЬ/bootstrap/di st/css/* .min. css " rel="stylesheet " /> </head>
662 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC <body class="panel-body"> @RenderSection("Header") <d iv class="bg-info"> This is part of the layo ut </ div> @RenderBody () <d iv class="bg-info"> This is part of the layout </div> @RenderSection("Footer") <d iv class="bg-info"> This is part of the layout </div> </body> </html> Когда Razor производит разбор компоновки, выражение @RenderSection заменя­ ется содержимым раздела с указанным именем из представления. Части представле­ ния, которые не содержатся в каком-либо разделе, вставляются в компоновку с ис­ пользованием выражения @RenderBody. Запустив приложение, можно увидеть эффект от применения разделов (рис. 21.5). Чтобы было совершенно ясно, какие разделы вывода поступают из представления, а какие - из компоновки, используются стили Bootstrap. Результат далек от идеала, но четко демонстрирует, как можно помещать области содержимого из представления в специфические местоположения внутри компоновки. 1 1 This is part о! the layout 1 This is а list of fruil names: Apple Orange Pear 1 1 This is part of the fayout lThis is the footer 1 This is part of the layout -· -···-- -------------" -----·- - ------ ------""---J Рис. 21.5. Использование разделов в представлении для размещения содержимого в компоновке На заметку! В представлении можно определять только разделы, на которые имеются ссыл­ ки внутри компоновки. При попытке определить в представлении разделы, для которых отсутствуют соответствующие выражения @RenderSection в компоновке, инфраструк­ тура MVC сгенерирует исключение.
Глава 21. Представления 663 Смешивание разделов с остальной частью предст авления обычно не делается. По соглашению разделы определяются либо в начале, либо в конце представления, что­ бы легче было видеть, какие области содержимого будут трактоваться как разделы, а какие будут обслуживаться выражением @RenderBody. Еще один подход предусмат­ ривает определение представления полностью в терминах разделов, включая раздел для тела (листинг 21.15). Листинг 21.15. Определение представления в терминах разделов Razor в файле Index. cshtml @model string[] @{ Layout = " Layout "; @section Header { <div class="bg -s uccess " > @foreach (string str in new [] {"Ноше", "List ", "Edit"}) { <а class= " Ьtn Ьtn -s.m Ьtn-primary " asp - action= " str " >@str</a> </div> @section Body This is а list of frui t names: @foreach (string name in Model) <span><b>@name</Ь></span> @section Footer { <div class= " bg-success " > This is the footer </div> При таком подходе получаются более чистые представления и уменьшаются шан­ сы захвата излишнего содержимого выражением @RenderBody. Чтобы задейство­ вать этот подход , выражение @RenderBody понадобится заменить выражением @ RenderSection ( " Body " ), как показано в листинге 21.16. Листинг 21.16. Визуализация тела в виде раздела в файле_Layou t. csh tml <!DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>@ViewBag.Title</title> <link asp-href - include= " liЬ/bootstrap/dist/css/* . min . css " rel= " stylesheet " /> </head> <body class= " panel - body " > @RenderSection( " Header " ) <div class="bg- info"> This is part of the layout </div>
664 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC @RenderSection ( "Body") <div class= " bg -info " > This is part of the layout </div> @RenderSection ( " Footer " ) <div class= " bg-info " > This is part of the layout </div> </body> </html> Проверка существования разделов При необходимости можно выполнять проверку, определен ли в предст авлении специфический раздел из компоновки. Это удобный способ предоставления стандар­ тного содержимого для раздела, если представление не нуждается или не желает пре­ доставлять специальное содержимое. В листинге 21.17 приведено модифицированное содержимое файла _Layout . cshtml , в котором предпринимается проверка, опреде­ лен ли раздел Footer. Листинг 21.17 . Проверка, определен ли раздел в представлении, в файле _Layout. cshtml <!DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>@ViewBag .Ti tle</title> < l ink asp - href - include= " liЬ/bootstrap/dist/css/* .min. css " rel= " stylesheet " /> </head> <body class= " panel -body " > @RenderSection( " Header " ) <div class=" bg- info"> This is part of the layout </div> @RenderSection( " Body " ) <div class= " bg-info " > This is part of the layout </div> @if (IsSectionDefined("Footer")) @RenderSection (" Footer") }else{ <h4>This is the default footer</h4> } <div class=" bg- info" > This is part of the layout </div> </body> </html>
Глава 21. Представления 665 Вспомогательный метод IsSectionDefined () принимает имя проверяемого раздела и возвращает true, если он определен в визуализируемом представлении. В настоящем примере IsSectionDefined () применяется для выяснения, должно ли визуализироваться стандартное содержимое, когда в представлении не определен раздел Footer. Визуализация необязательных разделов По умолчанию представление должно содержать все разделы, для которых в ком­ поновке имеются выражения @RenderSection. Если разделы отсутствуют, тогда ин­ фраструктура MVC сгенерирует исключение. Чтобы удостовериться в этом, добавьте в файл _Layout. cshtml новое выражение @RenderSection для раздела по имени scripts (листинг 21.18). Листинг 21.18. Визуализация несуществующего раздела в файле_Layout. cshtml <!DOCTYPE html> <html> <head> <meta name= " viewport" content= " width=device - width " /> <title>@ViewBag.Title</title> <link asp-href-include= "liЬ/bootstrap/dist/css/*.min.css" rel="stylesheet " /> </head> <body class="panel -b ody " > @RenderSection("Header ") <div class= " bg -i nfo"> This is part of the layout </div> @RenderSection("Body " ) <div class= " bg-info " > This is part of the layout </div> @if (IsSectionDefined("Footer")) @RenderSection("Footer " ) else { <h4>This is the default footer</h4> @RenderSection ( "scripts") <div class="bg-info " > This is part of the layout </div> </body> </html> После запуска приложения механизм Razor попытается визуализировать компо­ новку и представление, в результате чего возникает ошибка (рис. 21.6).
666 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC [.:J lnt~ mol S~IV<r Erro r Х С ~ locall1o>t:SS775 . '{::J Гдn-~~handled except ion occu~~~d \tVhil~ pгocessin; th; request . lrlvaliclOperatio n Excep iоп : Тfle layoLJt P.age 1 '/Views/Sfla recJ / _Layout.cs html' са not fiпa the sectioп 'su ipts ' in iJ1e coпtent page 'J\l ie\'IS/Home/ l пdex.csl1tm l'. MoveNoxt •1 '·1 1 1 1 ~: __Qll.::_ Cookies __ Headers ------·---- _______: .J Рис. 21.6. Сообщение об ошибке, отображаемое из-за отсутствия раздела Можно задействовать метод IsSectionDefined ( ) , чтобы избежать выполнения выражений @RenderSection для разделов, которые не определены в представлении, но более элегантный подход предусматривает использование необязательных разде­ лов, для которых в @RenderSection указывается дополнительный аргумент false (листинг 21.19). Листинг 21.19. Создание необязательного раздела @RenderSection("scripts ", false ) В итоге создается необязательный раздел, содержимое которого будет вставлено в результат, если представление определено, а его отсутствие не приведет к генерации исключения. Использование частичных представлений Часто возникает необходимость в применении одних и тех же фрагментов дескрип­ торов Razor и разметки HTML в нескольких местах приложения. Вместо дублирова­ ния содержимого можно использовать частичные представления - отдельные файлы представлений, содержащие фрагменты дескрипторов и разметку, которые могут быть включены в другие представления. В настоящем разделе будет показано, как создавать и применять частичные представления, объяснена их работа и продемонстрированы приемы, доступные для передачи данных представления в частичное представление. Создание частичного представления Частичные представления являются всего лишь обычными файлами CSHTML, отличаясь от нормальных представлений только способом их использования. Среда Visual Studio предлагает инструментальную поддержку для создания заранее на­ полненных частичных представлений, но создать частичное представление проще всего, создав обычное представление с применением шаблона элемента MVC View Page (Страница представления МVС). Добавьте в папку Views/Home файл по имени MyPartial . cshtml и поместите в него содержимое из листинга 21.20.
Глава 21 . Представления 667 Листинг 21.20. Содержимое файла MyPartial. cshtml из папки Views/Home <div class= " bg -info " > <div>This is the message from the partial view . </div> <а asp-action="Index">This is а link to the Index action</a> </div> Здесь планируется продемонстрировать возможность смешивания в частичном представлении статического и динамического содержимого, поэтому было определено простое сообщение и якорный элемент, в котором используется дескрипторный вспо­ могательный класс. Применение частичного представления Частичное представление потребляется посредством выражения @Html. Partial внутри другого представления. Создайте в папке Views/Home файл по имени List . cshtml и добавьте в него содержимое, показанное в листинге 21.21 . Листинг 21.21. Содержимое файла List. cshtml из папки Views/Home @( Layout = null ; < ! DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device - width " /> <title>Razor</title> <link asp-href-include= 11 liЬ/boo tstrap/di st/ css/* . min . css 11 rel= 11 stylesheet 11 /> </head> <body cl ass=" panel-body"> This is the List View @Html . Partial( "MyP artial ") </body> </html> Метод Partial () - это расширяющий метод, применяемый к свойству Html , ко­ торое добавл ено в класс, сгенерированный механизмом Razor из файла представле­ ния. Он является примером вспомогательного метода HTML, который использовался в качестве способа генерирования динамического содержимого внутри представле­ ний в предшествующих версиях MVC, но теперь в значительной степени замещен де­ скрипторными вспомогательными классами . Методу Partial () передается аргумент, указывающий имя частичного представления, содержимое которого вставляется в вывод, отправляемый клиенту. Совет. Механизм Razor ищет частичные представления там же, где и обычные представле­ ния (в папках -/Viеws/<контроллер> и -/Views/Shared). Другими словами, мож­ но создавать специализированные версии частичных представлений, специфичные для контроллера, и переопределять частичные представления с таким же именем из папки Shared. Запустив приложение и перейдя на URL вида /Home/List, можно оценить эффект от потребления частичного представления (рис. 21. 7).
668 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC С ~~os1:5577 5/Home/list --- -- n 1is is the List View This is the. message from th e pa rtial view. This is а link to the lndex action R-1 . ----· J: Рис. 21.7. Применение частичного представления Использование строго типизированных частичных представлений Можно создавать строго типизированные частичные представления и снабжать их объект ами модели представления, подлежащими применению при визуали зации частичных представлений . Создайт е в папке Views/ Home ф айл представл ен ия по имени MyStro nglyType dPa r tial. c s html и поместите в него содержимое из лис ­ тинга 21 .22. Листинг 21.22. Содержимое файла MyStronglyTypedPartial. cshtml из папки Views/Home @model I EnumeraЫ e <s tr ing> <div cl ass="bg-info"> This is the message from the partial view. <ul > @foreach (stri ng str in Model) { <l i >@str</li> </ul> </div> С использованием стандартного выражения @mod e l определяется тип модели пред ­ ставления , а с помощью цикла @f o rea c h отображается содержимое объе1па модели представления в виде элементов списка HTML. Чтобы задействовать новое частичное представление, модифицируйте содержимое файла /Views/Commo n/Lis t. cshtml, как показано в листин ге 21.23. Листинг 21.23. Применение строго типизированного частичного представления в файле List. cshtml @{ Layout = null ; <!DOCT YP E h tml> <html > <head> <meta nam e = " viewport " cont e nt= "width=de vice-width " /> <ti t le>Razor</title> <link asp-href - include =" liЬ/boots t rap/dis t / css/* .mi n . css " rel="s tylesheet " / > </head>
<body class="p a nel-b o dy"> This is the List View @Html.Partial("MyStronglyTypedPartial", new string[] { "Apple", "Orange", "Pear" }) </body> </ html> Глава 21. Представления 669 Отличие от предыдущего примера заЮiючается в том, что здесь вспомогательному методу Part ial () передается дополнительный аргумент, который поставляет модель представления. Запустив приложение и перейдя на URL вида /Horne/List, можно пос­ мотреть на работу строго типизированного частичного представления (рис . 21.8). This is the List View This is the message from the pa rt ial view. • Apple • Orange • Pear Рис. 21.8 . Использование строго типизированного частичного представления Добавление содержимого JSON в представления Содержимое JSON часто включается в представления для того, чтобы снабдить код JavaScript клиентской стороны данными, которые могут применяться при дина­ мической генерации содержимого. Для подготовки к такому примеру добавьте в при ­ ложение пакет jQuery, отредактировав файл bower. j son согласно листингу 21.24. П акет jQuery облегчит обработку данных JSON, когда они получаются браузером как часть НТМL-документа. Листинг 21.24. Добавление пакета jQuery в файле bower. j son "name": "asp.net", "priva te ": true , " dependencies" : { "bo o tstrap": "З.3.6", "jquery": 11 2.2 .4 11 В листинге 21.25 приведены добавления внутри представления List . cshtrnl, которые используют Razor для включения данных JSON в ответ, отправляемый браузеру.
670 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 21.25. Работа с данными JSON в файле List.cshtml @{ Layout = null; < ! DOCTYPE html> <html> <head> <meta name="viewport" content= "width=device - width " /> <title>Razor</title> <link asp-href-include= " liЬ/bootstrap/dist/ css/* .min. css " rel= " stylesheet " /> <script id="jsonData" type="application/json"> @Json.Serialize(new string[] { "Apple", "Orange", "Pear" }) </script> </head> <body class= " panel - body " > This is the List View <ul id="list"></ul> </body> </html> Выражение @Json . Serialize принимает объект и сериализирует его в формат JSON. В листинге 21 .25 к представлению добавлен элемент script, содержащий дан­ ные JSON . Когда представление визуализируется и отправляется браузеру, оно вклю­ чает элемент, подобный показанному ниже: <script id="jsonData" type="application/json">["Apple","Orange", "Pear"J </script> Чтобы можно было работать с данными JSON, к представлению List . cshtml до­ бавляется библиотекаjQuеrу и встраиваемый код JavaScript, который прим е няет ее для разбора данных JSON и динамического создания НТМL-элементов (листинг 21.26). Листинг 21.26. Использование данных JSON в файле List. cshtml @{ Layout = null; <!DOCTYPE html> <html > <head> <meta name="viewport" content= " width=device - width " /> <title>Razor</title> <link asp-href - include= "liЬ/ bootstrap/dist/css/*.min . css " rel="stylesheet" /> <script id= "j sonData" type= " app l ication/json " > @Json.Serialize(new string[ ] { "Apple ", " Orange" , "Pear" }) </script> <script asp-src-include="lib/jquery/dist/*.min.js"></script> <script type="text/javascript"> $(document) .ready(function () { var list = $ ("#list") JSON. parse ($ ( "#j sonData" ) . text () ) . f orEach (function (val) { console. log ( "Val: " + val) ; list. append ($ ( "<li>") . text (val)) ; }); }); </script> </head>
<body class= " panel - body " > This is the List View <ul id="li st"></ul> </body> </html> Глава 21 . Представления 671 По сле запуска приложения и запроса URL вида /Horne/List отобразится содержи­ мое, показанное на рис. 21.9 . Это не самая впечатляющая работа с данными JSON, но она демонстрирует, как их можно включать в представления. 1 1 This is the List View • Apple 1 • Orange 1 ~е_а_г~~--~~· Рис. 21.9. Использование данных JSON в представлении Конфигурирование механизма Razor Механизм Razor можно конфигурировать с применением класса Ra zo rViewEngine Options , который находится в пространстве имен Microsoft . AspNet Core . Mvc . Razor . Класс определяет два конфигурационных свойства, описанные в табл. 21.9 . Таблица 21.9 . Свойства класса RazorViewEngineOptions Имя FileProvider ViewLocationExpanders Описание Это свойство используется для установки объекта, который предоставляет механизму Razor содержи- мое файлов и каталогов. Функциональность опре­ деляется интерфейсом Microsoft . AspNetCore . FileProviders. IFileProvider, а стандартной реализацией является класс PhysicalFileProvider, который обеспечивает чтение файлов из диска Это свойство применяется при конфигурировании рас­ ширителей местоположений представлений, которые используются для изменения способа поиска представ ­ ления механизмом Razor
672 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге мvс Совет. Если вы действительно заинтересованы в тщательном исследовании, тогда мо­ жете заменить внутренние компоненты Razor, создав классы, к оторые реализуют интерфейсы из пространства имен Microsoft . AspNetCore . Mvc. Razor, и за­ регистрировав их с помощью поставщика служб в классе Startup. У большинства раз­ работчиков никогда не возникнет необходимость выполнять такую работу, и к ней нельзя относиться легкомысленно. Первым делом понадобится загрузить исходный код Razor из http : //g ithuЬ. com/aspnet. Свойство FileProvider не относится к тем свойствам, которые будут изм е нять многие приложения , поскольку чтение файлов пр едставлений из диска явля ется в точности тем, что требуется в большинстве проектов. Механизм Razor исполь­ зует данный поставщик только для загрузки представлений, чтобы их можно было скомпилировать , когда приложение запускается в первый раз. Однако сво йств о ViewLocationExpanders более полезно, т.к. оно позволяет применять специальную логику в отношении способа , которым Razor ищет представления . Расширители местоположений представлений Расширители местоположений представлений используются механизмом Razor для построения списка мест, в которых должен вестись поиск представления. Расширители местоположений представлений реализуют интерфейс I ViewLocationExpander , оп­ ределенный следующим образом: u sing Syst em . Collections . Generic ; namespa c e Microsoft . AspNetCore . Mvc . Razor puЫic interface IViewLocationExpander { void PopulateValues(View LocationExpanderContext context) ; IEnumeraЫe<string> ExpandV iewLoca tions( ViewLoc at i on Exp anderCo n text co n text , I Enume r aЫe<string> v iewLocations) ; В приведенных дале е разделах объясняется, как работают расширители м ес­ тоположений представлений , и создается специальная реализация интерфейса IViewLocationExpander . Для подготовки к созданию расширителей местополо­ жений представлений потребуется изменить метод действия Index {) контроллера Ноте, чтобы он запрашивал несуществующее представление (листинг 21 .27). В ре­ зультирующем сообщении об ошибке будут присутствовать местоположения, в кото­ рых механизм Razor искал представление , и отражен эффект, оказываемый на них расширителями местоположений представлений . Листинг 21.27. Запрашивание несуществующего представления в файле HomeController. cs using System; using Microsoft.AspNetCore . Mvc; namespace Views .Controllers { puЫic class HomeCo ntr ol l er : Controller puЬlic ViewResul t Index () => View("MyView", newstring[] { "Apple", "Orange", "Pear" }); puЫic ViewResult List() => View() ;
Глава 21. Представления 673 Запустив приложение и запросив стандартный URL, вы увидите, что в сообщении об ошибке отображаются стандартные местоположения поиска представления: /Views/Horne/MyView.cshtml /Views/Shared/MyView.cshtrnl Создание простого расширителя местоположений представлений Простейший расширитель местоположений представлений всего лишь изменя­ ет набор мест, где Razor проводит поиск всех представлений. Для этого необходимо реализовать метод ExpandViewLocations () и возвратить список местоположений, которые должны поддерживаться. Добавьте в папку Infrastructure файл клас­ са по имени SirnpleExpander. cs и поместите в него определение, показанное в листинге 21.28. Листинг 21.28. Содержимое файла SimpleExpander. cs из папки Infrastructure using Systern.Collections.Generic; using Microsoft . AspNetCore.Mvc.Razor ; namespace Views . Infrastructure puЫic class SimpleExpander : IViewLocationExpander puЫic void PopulateValues(ViewLocationExpanderContext context) { //ничего не делать - метод не требуется puЬlic IEnumeraЫe<string> ExpandViewLocations( ViewLocationExpanderContext context, IEnumeraЫe<string> viewLocations) { foreach (string location in viewLocations) yield return location.Replace("Shared", "Common"); yield return " /Views/Legacy/{1}/{0}/View . cshtml"; Механизм Razor вызывает метод ExpandViewLocations ().когда ему нужен спи­ сок местоположений поиска, и предоставляет стандартные местоположения как пос­ ледовательность строк в параметре viewLocations. Местоположения выражаются в виде шаблонов с заполнителями для имен действия и контроллера. Вот как выглядят шаблоны местоположений, которые применяются по умолчанию в приложении, не использующем области маршрутизации: " /Views/{1}/{0} . cshtml " " /Views/Shared/{0}.cshtml" Заполнитель {О} применяется для ссылки на имя метода действия, а заполнитель {1 } - для ссылки на имя контроллера. Работа расширителя местоположений пред­ ставлений заключается в возвращении набора мест, в которых должен производить­ ся поиск , и в листинге 21.28 используется метод string . Replace () для изменения Shared на Comrnon в стандартных местоположениях, а также добавляется собствен­ ное местоположение с другой структурой файлов и папок.
674 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Применение расширителя местоположений представлений В листинге 21.29 расширитель местоположений представлений настраи­ вае тся путем конфигурирования м еха низ м а Razor в класс е Startup. С в о й ство ViewLocationExpanders во звращает объект Li st<IViewLocationExpander>, на котором вызывается метод Add ( ) . Листинг 21.29. Конфигурирование механизма Razor в файле Startup. cs us ing Microsoft.AspNet Core . Bui lder; using Microsoft . Extensions . Dependencyinjection ; u si ng Microsoft . AspNetCore . Mvc ; us ing Views . Infrastructure ; using Microsoft.AspNetCore.Mvc.Razor; namespace Views { pu Ы ic class S tartup puЫic void ConfigureServi ces(ISe r v i ceCo l lection se r vices) services.AddMvc() ; services . Confi gure<RazorViewEngineOptions>(options => { options.ViewLocationExpanders.Add(new SirnpleExpander()); }); puЫic void Confi gur e(IAppl ication Buil d e r арр) { app . UseSta t usCode P ages() ; app . UseDeveloperExcept i onPage() ; app . UseStat i cFiles() ; app . UseMvcWithDefau l tRoute() ; По сл е запуска приложения сообщение об ошибке отобразит набор местоположе­ ний, который был предоставлен механизму Razor специальным расшир ителем ме сто­ положений пр едставлений: /View s/ Home/MyView . cshtml /Vi ew s /Common/MyView . cshtml /Views/Legacy/Home/MyView/View . cshtml Выбор специфических представлений для запросов Расширители местоположений представлений облегчают изм енение мест поиска для всех запросов, но могут такж е делать это для индивидуальных запросов. В преды­ дущем примере был р еализован только метод ExpandViewLocations () , но реальную мощь демонстрирует метод PopulateValues (), являющийся еще одним мето до м в интерф ейсе IVi ewLocationExpan der . Всякий раз , когда механизму Razor требуется представл ение , он вы зывает ме­ тод Popu l ateValues () своих расширит елей м е стополо же ний пр едставл е ний. пер едавая ему объ е кт Vi ewLocationE x panderContext для данны х нонт е кста . В класс е ViewLo c ationExpanderConte x t определены свойст в а, перечисл енны е в табл. 21 . 10.
Глава 21. Представления 675 Таблиц а 21. 10. Свойства класса ViewLocationExpanderContext Имя ActionContext ViewName ControllerName AreaName IsMainPage Values Описание Это свойство возвращает объе кт ActionContext , который описы­ вает метод действия, запросивший представление, а также включа­ ет детали запроса и ответа Это свойство возвращает имя представления , которое запросил метод действия Это свойство возвращает имя контроллера, который содержит ме ­ тод действия Это свойство возвращает и м я области, содер ж ащей контроллер , если области были определены Это свойство возвращает false , если ме х анизм Razor ищет час­ тичное представление, и tru e в противном случае Это свойство возвращает объект IDi ctionary<string , string>, к которому расширитель местоположений представлений добавляет пары "ключ-значение " , уникально идентифицирующие категорию за­ проса, ка к объясняется ниже Мето д PopulateValues () предназначен для к атегор изаци и запрос а за счет до­ бавления пар "ключ - значени е" в словарь , возвращаемый свойством Va l ues объекта контекста. Механизм Razor не беспокоит то, как категоризируется запрос , и реал и­ зация метода для заполнения словаря возложена полностью на расширитель место­ положе ний пр ед ставлений . Легче всего это объяснить с помощью прим ера, так что добавьте в папку Infrastructu r e файл класса по имени ColorExpander . cs с опр е ­ дел ением из листинга 21 .30 . Ли сти нг 21.ЗО. Содержимое файла ColorExpander. cs из папки Infrastructure using System.Collections . Gener i c ; using Microsoft . AspNe t Core . Mvc . Razor ; namespace Views .I nfrastructu r e { puЫic class ColorExpander : I ViewLocationExpander private static Dictionary<string, string> Co l ors = new Dictiona r y<string , string> { ["red"] = "Red", [ "green"] = "Green", ["Ыuе"] = "Blue" }; puЫic void PopulateVa l ues(ViewLocation ExpanderCon text context) var routeValues = context . Act i onContext . RouteData . Values ; string color ; if ( r outeValues . ContainsKey( " id " ) && Colors . TryGe t Value(routeValues [" id " ] as stri n g , out color) && !stri ng.IsNul lOrEmpty(color )) { context .Values [ "color"] = col or;
676 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core мvс puЬlic IEnumeraЫe<st ri ng> Expan dV i ewLocat i ons( ViewLoca t ionExpanderContext context , IEnumeraЫe<string> viewLocations) { string color ; context . Values .T ryGetValue( " col o r", out color) ; foreach (string location in viewLocat ions) { if ( ! str i ng . IsNullOrEmpty(color)) { yield return location . Rep l ace( " {O) ", color) ; else { yield r eturn locat ion ; Метод Po p ulateVa l ues () исполь зу ет объект Act i onContext для получ е ния дан­ ных маршрутизации и ищет значение сегмента id в URL. Если есть сегмент id и его значени ем является red, green или Ыuе , тогда расширитель местополож ений пр ед­ ставлен ий добавляет в словарь Va l ues свойство color . Это процесс категори заци и: запрос, сегмент id которого соответствует цвету , категоризируется с кл ючом color, чье значение выводится из значения сегмента. Дале е м еханизм Razor вызывает метод ExpandViewLocations () и пр едоставля ­ ет тот же объект контекста, который применялся для метода PopulateValues () . Это позволяет расширителю местоположений представлений просматривать про­ веденную ран ее категори зацию и генерировать н абор м ест, в которых м еханиз м Razor должен искать пр едст авления. В рассматр иваемо м пример е с помощью м етода string . Replace () заполнитель {О J заменялся им енем цвета. Совет. Ме ханизм Razor вызывает метод Pop u lateValues () для каждого запроса пред­ ставления, но кеширует набор местоположений поиска, возвращаемый методом ExpandViewLocat i ons () .Таким образом , последующие запросы, для к оторы х метод PopulateVa l ues () генерирует тот же самый набор ключей и значений категоризации, не требует вызова метода ExpandVi ewLocat i o n s (). В листинге 21.31 меха низм Razor 1<0нфигурируется для исполь з ов а ния кл а сс а Co l orExpander. Листинг 21.31. Добавление расширителя местоположений представлений в файле Startup. cs puЫic void Configu r eSe r vices( I Servi ceCol l ec t ion services) se r vices.AddMvc() ; services.Configure<RazorVi ew EngineOptions>(opt i o n s => { options . ViewLocationEx pande r s.Add( n ew SimpleExpander()) ; options.ViewLocationExpanders.Add(new ColorExpande r()); });
Гл ава 21. Представ л ения 677 Запустив приложение и запросив URL вида / Ho me/Index/red , можно заметить эффект от нового расширит еля местоположений представлений, который приведет к тому, что механизм Razor будет выполнять поиск в следующих местах: /V i ews/Home/Red . cshtml /Views/Common/Red . csh t ml /Vi ews/Legacy/Home/Red/View . c shtml Аналогично запрос URL вида /Home/Index /gr een заставит Razor искать в таких м е стополож е ниях: /Views/Home/Green.cshtml /Views/Common/Green . cshtml /Views/Legacy/Home/G r een/Vi e w . cs h tml Порядо1<, в 1<отором зарегистрированы расширители местоположений пред­ ставл ений, важен, потому что набор местополож е ний, генерируемый методом ExpandVie wLocat i o n s ( ) одного расширителя, применяется в качестве аргумента viewLocations для следующего расширителя в списке . Это можно заметить в по­ казанных ран ее местополож ениях, где Vi e ws/ Common и Views/Le g a c y генерирова­ лись классом SimpleExpan de r, 1<оторый находился перед Co l orExp ander в классе Startup . Резюме В настоящей главе было продемонстрировано, 1<а1< создавать специальный ме­ ханизм визуализ ации , и объя снялось, каким образом работает механизм Razor при трансляции файлов CSHTML в классы С# . Вы узнали, как исполь:Зовать разделы компоновки и частичные пр едставления, и научились изменять местоположения, в которых Razor ищет файлы предс тавлений. В следующей главе будут описаны ком­ поненты пр едставлений, которые прим еняются для обеспечения логи1<и поддержки частичных представлений .
ГЛАВА 22 Компоненты v представлении в настоящей главе рассматриваются компоненты представлений. которые яв­ ляются новым добавлением к инфраструктуре ASP.NEТ Core MVC и заменяют собой средство дочерних действий из предшествующих версий . Компоненты пред­ ставлений - это классы, предлагающие логику в стиле действий для поддержки час­ тичных представлений. Это означает возможность встраивания сложного содержимо ­ го в представления и вместе с тем простое сопровождение и модульное те стировани е кода С#, который его поддерживает. В табл. 22. l приведена сводка, позволяющая по­ местить компоненты представлений в контекст. Таблица 22.1 . Помещение компонентов представлений в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие­ то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по срав­ нению с версией MVC 5? Ответ Компоненты представлений являются классами , которые обес­ печивают прикладную логику для поддержки частичных пред­ ставлений либо для внедрения небольших фрагментов HTML· разметки или данных JSON в родительское представление Без компонентов представлений трудно создать встроенную функциональность, такую как корзина для покупок или панель входа, способом, который допускает легкое сопровождение и модульное тестирование Компоненты представлений обычно являются производными от класса ViewComp o n ent и применяются в дочернем пред­ ставлении с использованием выражения @awaitComponent .I nvokeAsync Нет, компоненты представлений - это простое и предсказуе­ мое средство. Основное заблуждение связано с отказом от и х применения и попыткой включить прикладную логику внутрь представлений, где их трудно тестировать и сопровождать Можно было бы поместить логику доступа и обработки данных прямо в частичное представление, но в результате работа с ней и эффективное тестирование станут значительно труднее Компоненты представлений являются новым средством в инф­ раструктуре ASP.NET Core MVC, пришедшим на замену средс­ тву дочерних действий из предыдущих версий
Глава 22 . Компоненты представлений 679 В табл . 22.2 приведена сводка для настоящей главы. Таблица 22.2 . Сводка по главе Задача Решение Создание частичного представления Используйте компонент представления с собственной логикой и данными Вызов компонента представления Применяйте выражение Упрощение доступа к данным кон­ текста и результатам Выбор частичного представления Создание фрагмента НТМL­ разметки Использование деталей запроса для генерирования результата Предоставление данных кон­ текста при вызове компонента представления Создание асинхронного компонен­ та представления Создание гибридного компонента контроллера/представления @await Cornponent . InvokeAsync в представлении Унаследуйте класс от класса ViewCornponent Используйте метод View () для создания и возвращения объекта ViewViewCornponentResult Вызовите метод Content () для создания объекта ContentViewCornponentResult или явно создайте объект HtrnlContentViewCornponentResult, если не хотите, чтобы фрагмент был закодирован Применяйте данные контекста компо­ нента представления Передайте аргументы методу InvokeAsync () Реализуйте метод InvokeAsync () и возвратите объект Task, который выдает требуемый результат Применяйте атрибут ViewCornponent к классу контроллера Подготовка проекта для примера Листинг 22.1-22 .13 22.14 22.15, 22.16 22.17 -22.19 22.20, 22.21 22.22 22.23-22.25 22.26-22.29 22 .30 -22 .33 Создайте новый проект типа Empty (Пустой) по имени UsingViewCornponents с ис­ пользованием шаблона ASP.NET Саге Web Application (.NET Саге) (Веб-приложение ASP. NЕТ Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies файла proj ect . j son и настройте инструментарий Razor в разделе tools, как показано в лис­ тинг е 22.1 . Разделы, которые не нужны для данной главы, понадобится удалить. Листинг 22.1 . Добавление пакетов в файле proj ect. j son " dependencies ": { " Microsoft.NETCore .App ": "version": "1 .0 .0", " type ": " platform " }'
680 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC }, " Microsoft.AspNetCore.Diagnostics": "1.0 .0", " Microsoft . AspNetCore . Server . IISintegration" : " 1.0.О ", " Microsoft . AspNetCore.Server . Kestrel ": " l . 0 . 0 ", " Microsoft . Extensions.Logging.Console": "1. 0 . О ", "Microsoft.AspNetCore . Mvc": 11 1.0.0 11 , "Microsoft.AspNetCore.StaticFiles": 11 1.0.0 11 , "Microsoft.AspNetCore . Razor.Tools": { "version": "1.0 .0 -preview2-final", "type": "build" " tools ": " Microsoft . AspNetCore.Server . IISintegration . Tools ": "1. 0 . 0 - preview2 - final ", "Microsoft.AspNetCore .Razor.Tools": "l.0.0-preview2-final" }, " frameworks ": { " netcoreappl.0": " imports ": [ " dotnet5 . 6 ", " portaЫe-net45+win8 "] }, " buildOptions ": " emitEntryPoint ": true , " preserveCompilationContext": true }, " runtimeOptions": { " configProperties " : { " System . GC .Se rver ": true } Создание моделей и хранилищ Для демонстрации работы компонентов представлений понадобятся два источни­ ка данных. Часть приложения будет оперировать с набором описаний товаров; чтобы подготовиться к этому. создайте папку Models и добавьте в нее файл класса по имени Product . cs с определением из листинга 22.2 . Листинг 22.2 . Содержимое файла Product. cs из папки Models namespace UsingViewComponents . Models puЫic class Product { puЫic string Name { get ; set ; } puЫic decimal Price { get; set ; Чтобы создать хранилище для объектов Product, добавьте в папку Models файл по имени ProductReposi tory. cs и определите в нем интерфейс и класс реализа­ ции, как показано в листинге 22.3 .
Глава 22 . Комnоненты nредставлений 681 Листинг 22.З. Содержимое файла ProductRepository. cs из папки Models using System.Collections . Generic; namespace UsingViewComponents . Models puЬlic interface IProductRepository { IEnumeraЫe<Product> Products { get; void AddProduct(Product newProduct); puЫic class MemoryProductRepository : IProductRepository private List<Product> new Product { Name new Product { Name new Product { Name }; products = new List<Product> { "Kayak", Price = 275 М }, "Lifejacket", Price = 48 . 95 М }, "Soccer ball", Price = 19.50 М} puЫic IEnumeraЫe<Product> Products => products; puЫic void AddProduct(Product newProduct) { products . Add(newProduct); В интерфейсе IProductReposi tory определен ограниченный набор средств хра­ нилища, а класс MemoryProductReposi tory реализует данный интерфейс с приме­ нением объекта List, находящегося в памяти. Другая часть приложения будет опе­ рировать с описаниями городов. С этой целью добавьте в папку Models файл класса по имени City . cs, содержимое которого приведено в листинге 22.4 . Листинг 22.4 . Содержимое файла Ci ty. cs из папки Models namespace UsingViewComponents.Models puЬlic class City { puЫic string Name { get; set ; } puЫic string Country { get; set; puЬlic int Population { get ; set; Для хранилища объектов Ci ty создайте в папке Models файл класса по имени Ci tyReposi tory . cs и определите в нем интерфейс и класс реализации, как показа­ но в листинге 22.5 . Листинг 22.5 . Содержимое файла Ci tyReposi tory. cs из папки Models using System . Collections . Generic; namespace UsingViewComponents . Models puЫic interface ICityRepository { IEnumeraЫe<City> Cities { get; } void AddCity(City newCity) ;
682 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic class MemoryCityRepository : ICityRepository { private List<City> cities = new List <City> { new City { Name = " London ", Country = "UK", Population 8539000} , 8406000 }, 998537 1 , 2244000 ) new City { Name = " New York ", Country = " USA ", Population new City { Name = " San Jose ", Country = " USA ", Population new City { Name = " Paris ", Country = "France", Population = ); puЫic IEnumeraЫe<City> Cities => cities ; puЫic void AddCity(City newCity) { c it ies . Add(newCity); Интерфейс ICi tyReposi tory предлагает огранич ен ный набор средств храни ­ лища. а класс MemoryCi tyReposi tory реали зует этот интерфейс с использованием объекта L ist. находящегося в памяти . Создание контроллера и представлений Для н ачала необходим только один контроллер. поэто му создайте папку Controllers . добавьте в нее файл по имени HomeController. cs и опр еделите в нем класс, приведенный в листинге 22.6 . Листинг 22.6 . Содержимое файла HomeController. cs из папки Controllers using Microsoft.AspNetCore . Mvc ; using UsingViewComponents.Models; namespace UsingViewComponents.Controllers puЫic c lass HomeController : Controller private IProductRepository repository; puЫic HomeController(IProductRepository repo) { repository = repo; puЬlic ViewResult Index() => View(repository . Products) ; puЫic ViewResult Create() => View() ; (HttpPost] puЫic IActionResult Create(Product newProduct) repository .AddProduct(newProduct) ; return RedirectToAction( " Index " ) ; Контроллер Home прим еняет свой конструктор для объявления зависимости от ин­ терфейса IProductReposi tory. которая будет распознаваться по ставщиком служб, когда контроллер станет использоваться при обработке запросов. Действие Index из ­ влекает все объекты Product из хранилища и визуализирует их с применением стан­ дартного представления. Два метода Create ( ) задействуют паттерн Post/Redirect/ Get при добавлении новых объектов в хранилище с использованием данных формы. передаваемых клиентом.
Глава 22. Комnоненты nредставлений 683 Пр едставления в рассматриваемом примере будут разделять общую компоновку. Создайте папку Views/Shared и добавьте в нее файл по имени Layout . cshtml с разметкой из листинга 22.7 . Листинг 22.7 . Содержимое файла _Lауоut. cshtml из папки Views/Shared <!DOCTYPE html> <html> <head> <meta name= " viewport " content= " wi dth=device-width " /> <title>@ViewBag . Tit l e</title> <link asp-href- incl ude= " liЬ/bootst r ap/dis t / css/* . min . css " rel=" stylesheet" /> </head> <body class= " panel -body " > <div class= 11 b g -primary panel -b ody " > <div class= "row" > <div class= 11 col- xs -7 " >< hl>Products</hl></div> <div class="col-xs-5 "> <div clas s="bg-info text - primary panel-body">City Placeholder</div> </div> </div> </div> <div c lass="panel-body">@RenderBody()</div> </body> </html> В компоновке определен заголовок, включающий заполнитель для содержимого, которое будет создано позже в главе с применением хранилища городов. Затем со­ здайт е папку Views/Home и поместите в нее файл по имени Inde x . cshtml с раз­ меткой, приведенной в листинге 22.8, которая выводит детали объектов Product в таблице. Листинг 22.8 . Содержимое файла Index. cshtml из папки Views/Home @model IEnume raЫe<Product> @{ ViewData[ " Title "] = " Products "; Layout = 11 Layout 11 ; <tаЫе class= " taЫe taЬle - condensed taЬle -st riped taЫe -b ordered 11 > <thead> <tr><th>Name</ th><th>Price</th></tr> </thead> <tbody> @foreach (var product in Mode l ) <tr> <td>@product .N ame</td> <td>@product .P rice</td> </tr> } </tbody> </tаЫе> <а asp - action ="Create" class= 11 Ьtn Ьtn - primary " >Crea te</a>
684 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Финальным элементом в представлении I ndex является элемент а. который сти­ лизован как :кнопк а и нацелен на действие Create , так что по ль з ователь мож ет со­ здавать новый объе1<Т P roduct в хранилище. Чтобы со здать форму . которую будет заполнять пользователь. добавьте в папку Views/Horne файл Create . cshtrnl с раз­ меткой из листинга 22.9 . Листинг 22.9 . Содержимое файла Create. cshtml из папки Views/Home @model Product @{ Vi ewData[ " Title "] Layo ut = " Layout "; " Create Produc t"; <form method= " post " asp - action= " Create " > <div class=" form- group"> <label asp - for= " Name " >Name : </labe l > <input cl ass=" form-control" asp-for="Name" /> </div> <div cl ass=" form- group"> <label asp-for= "Price">Pri ce :</l abel > <input class= " form - control" asp - for= "Price " /> </div> <b utton type= " subm i t " clas s=" Ьtn Ьtn - primary " >Create</button> <а c l ass= " Ьtn Ьtn - default " asp - action= " Index " >Cancel</a> </ f orm> В представлениях используются дес:крипторные вспомогательные классы. Чтобы сделать их доступными, создайте в папке Vi ew s файл _Viewirnports . cshtrnl и до­ бавьте в него выражения, показанные в листинге 22.10, которые также обеспечивают доступ к классам из папки Models без указания пространства имен. Листинг 22.1 О. Содержимое файла _Viewimports . cshtml из папки Views @usi ng UsingViewComponents . Mod els @a ddTagHelper * , Micr osoft .A spNetCor e . Mvc .T a gH elpers Представл ения такж е пол агаются на СSS-пак ет Boots tra p для стилиз ации сво его содержимого. Создайте в корневой пап1<е проекта файл bower. j son с применением шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па ­ кет Bootstrap в ра здел dependencies (листинг 22.11) . Листинг 22.11. Добавление пакета Bootstrap в файле bower. j son "name": "asp.net", " private ": true , "dependenci es": { "bootstrap": 11 3.3.6 11
Глава 22 . Компоненты представлений 685 Конфигурирование приложения Последний подготовительный шаг связан с конфитурированием приложения, как де­ монстрируется в листинге 22.12 . В дополнение к настройке служб и промежуточного про­ граммного обеспечения МVС создаются службы-одиночки для двух хранилищ данных . Листинг 22.12. Содержимое файла Startup. cs using Microsoft.AspNetCore . Bui l der ; using Microsoft.Extensions . Depe n dency in jection ; using UsingViewComponents.Models; namespace UsingViewComponents { puЫic c l ass Startup { puЫic void ConfigureServices(IServi ceCollection services) { services.AddSingleton<IProductRepository, MemoryProductRepository>(); services.AddSingleton<ICityRepository, MemoryCityRepository>(); services.Adc!Мvc(); puЫic void Co nfigure(IApplicationBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Запустив приложение , вы увидите список объектов Product из хранилища това­ ров. Можно добавлять новые товары, щелкая на кнопке Create (Со здать ) , заполняя форму и отправляя ее серверу, который затем перенаправит браузер обратно на спи­ сок (рис. 22.1) . Поскольку представл ения в приложении разделяют общую компонов­ ку, в течение всего процесса отображается заполнитель для данных по городам. D Pro<ivcis )( Products Na me: Name Price 275 Рис. 22.1. Выполнение примера приложения
686 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Понятие компонентов представлений Приложениям обычно необходимо встраивать в представления содержимое, ко­ торое не связано с главным назначением приложений. Распространенные примеры включают инструменты навигации по сайту, облака тегов и панели аутентификации, которые позволяют пользователю входить, не посещая отдельную страницу. Общая идея всех упомянутых примеров состоит в том, что данные, требуемые для отображения встроенного содержимого, не являются частью данных модели, которые передаются из действия в представление. Именно по этой причине в примере прило­ жения были созданы два хранилища: мы собираемся отображать содержимое, генери­ руемое с использованием хранилища объектов Ci ty, что нелегко делать в представле­ нии, которое получает от своих действий данные из хранилища объектов Product. В главе 21 было показано , каким образом частичные представления применя­ ются для создания многократно используемой разметки, которая требуется в при­ ложениях. избегая дублирования одного и того же содержимого во множестве мест приложения. Частичные представления - полезное средство, но они просто содер­ жат фрагменты НТМL-разметки и директивы Razor, а данные, с которыми они имеют дело , получаются от родительского представления. Если нужно отображать другие данные, тогда возникает проблема. Можно было бы получать доступ к необходимым данным прямо из частичного представления, но это нарушит принцип разделения обязанностей, который является фундаментом паттерна МVС, и приведет к помеще­ нию логики извлечения и обработки данных в файл представления, где она не может быть подвергнута модульному тестированию. В качестве альтернативы можно было бы расширить модели представлений, применяемые приложением, чтобы включить требующиеся данные, но тогда пришлось бы изменить каждый метод действия и тем самым затруднить изолирование функциональности методов действий для эффектив­ ного тестирования. Здесь на помощь приходят компоненты представлений. Компонент представле­ ния - это класс С#, который предоставляет частичное представление с необходимы­ ми ему данными, независимое от родительского представления и от ви зуализирую ­ щего действия. В таком отношении компонент представления можно трактовать как специализированное действие, которое используется только для доставки частичного представления с данными; оно не может получать НТГР-запросы, а выдаваемое им содержимое будет всегда включаться в родительское представление. Создание компонента представления Компоненты представлений можно создавать тремя способами: определяя компо­ нент представления РОСО, наследуя от базового класса ViewComponent и применяя атрибут ViewComponent . Приемы с классом РОСО и базовым классом описаны в пос­ ледующих разделах, а использование атрибута ViewComp onent объясняется в разде­ ле "Создание гибридных компонентов контроллеров/представлений" по зже в главе. Создание компонентов представлений РОСО Компонент представления РОСО- это класс, который поддерживает функциональ­ ность компонента представления, не полагаясь на какие-то АРI-интерфейсы MVC. Как и в случае контроллеров РОСО, с таким видом компонента представления неудобно ра­ ботать, но полезно знать особенности его функционирования. Компонентом представ­ ления РОСО является любой класс с именем, заканчивающимся на ViewComponent, в
Глава 22. Компоненты представлений 687 котором определен метод Invoke ().Классы компонентов представлений могут опре­ деляться где угодно в приложении, но по соглашению они собираются вместе в папке по имени Components, расположенной на корневом уровне проекта. Создайте ука­ занную папку и добавьте в нее файл класса PocoViewComponent. cs с определением, приведенным в листинге 22.13. Листинг 22.1 З. Содержимое файла PocoViewComponent. cs из папки Componen ts using Systern . Linq; using UsingViewCornponents.Models; narnespace UsingViewCornponents . ViewCornponents puЬlic class PocoViewCornponent { private ICityRepository repository; puЬlic PocoViewComponent(ICityRepository repo) { repository = repo; puЫic string Invoke() return $" {reposi tory. Ci ties. Count () ) ci ties, " + $"{repository . Cities.Surn(c => c . Population)} people"; Для получения требуемых служб компоненты представлений могут задействовать средство внедрения зависимостей. В рассматриваемом примере компонент представ­ ления РОСО объявляет зависимость от интерфейса ICi tyReposi tory, реализация которого затем применяется в методе Invoke () для создания объекта string, опи­ сывающего количество городов и суммарное число жителей. Для использования компонента представления требуется Rаzоr-выражение @awai t Component . Invoke. Компонент представления выбирается за счет предоставления в качестве аргумента имени иласса без окончания ViewComponen t. В листинге 22.14 из разделяемой компоновки удаляется заполнитель, а взамен применяется иомпонент представления РОСО. Листинг 22. 14. Применение компонента представления в файле _ Layout. cshtml <!DOCTYPE htrnl> <html> <head> <rneta narne="viewport" content="width=device - width " /> <title>@ViewBag . Title</title> <link asp-href-include="liЬ/bootstrap/dist/css/*.rnin . css" rel="stylesheet" /> </head> <body class="panel-body"> <div class="bg - prirnary panel -body " > <div class="row"> <div class="col-xs-7"><hl>Products</hl></div> <div class="col-xs -S ">@await Component. InvokeAsync ( "Росо") </div> </div> </div> <div class="panel-body">@RenderBody()</div> </body> </htrnl>
688 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Чтобы применить компонент представления, методу Invoke () в качестве аргу­ мента указывается имя Ро с о . Когда представление использует компоновку. оно нахо­ дит класс PocoViewCont r ol l er , вызывает его метод Invoke () и вставляет результат в вывод родительского представления (рис. 22.2). Pro ducts >< --- -·- -- --- --· ------·-·-------l > С l.so _1ocalt1ost:S7584 _ ~ Name Рис. 22.2. Применение простого компонента представления Хотя пример прост, он иллюстрирует ряд важных характеристик компонентов представлений. Во -первых , класс Poc oV iewCornp o n ent смог получить доступ к тре­ бующимся ему данным без какой-либо зависимости от обработки НТТР-запроса или от его родительского представления. Во-вторых, определени е логики, необходимой для получения и обработки сводки по городам, в классе С# означает, что он а может быть легко подвергнута модульному тестированию (за примером обращайтесь к врез­ ке "Модульное тестирование компонентов представлений " далее в глав е). В-третьих, форма приложения не нарушается из-за попытки включить объекты City в модели представлений, 1юторые ориентированы на объекты P rodu ct . Словом, компонент представл ения является самодостаточной порцией многократно используемой функ­ циональности , которая может применяться повсюду в приложении, к тому же разра­ батываться и тестироваться в изоляции. Внимание! При применении компонента представления в представлении должно быть ука­ зано ключевое слово aw ai t . В случае простого вызова @Co rn ponent . Invoke ошиб­ ка не возникнет, но отобразится строковое представление объекта Task , подобное такому: Systern. Threadi ng . Tas k s . Ta s k' 1 [Mi crosoft . AspNet Core . Html. IHtrn lConten t ]. Наследование от базового класса ViewComponent Компоненты представлений РОСО ограничены в функциональности , если только н е задействован АРI-интерфейс МVС, что вполне возможно, но тр е бует намного боль­ ших усилий, чем другой распространенный подход, предусматривающи й наследова­ ние от класса ViewComp o n ent. Класс Vi ewC orn p o n ent, определенный в пространстве имен Micro s oft. AspNetCore . Mvc , обеспечивает удобный доступ к данным контекс­ та и облегчает ген ерацию результатов. В листинге 22.15 приведено содержимое фай­ ла Ci t ySurnrna r y . cs , добавленного в папку Cornp onents.
Глава 22. Комnоненты nредставлений 689 Листинг 22.15. Содержимое файла Ci tySummary. cs из папки Componen ts using Systern . Linq; using Microsoft.AspNetCore.Mvc; using UsingViewCornponents.Models; narnespace UsingViewCornponents.Cornponents puЫic class CitySurnrnary : ViewCornponent { private ICityRepository repository; puЫic CitySurnrnary(ICityRepository repo) repository = repo; puЫic string Invoke() return $"{repository.Cities.Count()} cities, " + $"{repository.Cities.Surn(c => c . Population)} people"; При наследовании от базового класса ViewComponent указывать суффикс ViewComponent в имени класса не обязательно. За исключением использования базового класса этот компонент представления функционально идентичен версии РОСО. В последующих разделах будет показано, как применять удобные средства, предлагаемые базовым классом для работы с различными функциями компонентов представлений. Совет. Обратите внимание, что в листинге 22.15 метод Invo ke () не переопределялся. Класс ViewComponent не предоставляет стандартной реализации метода Invoke (), который должен быть определен явным образом. В рамках подготовки к демонстрации функций компонентов представлений из­ мените компонент, используемый в разделяемой компоновке (листинг 22.16). Вместо указания литеральной строки с именем компонента представления можно применять выражение nameof, описанное в главе 4, что сократит шансы неправильно набора имени класса. Листинг 22.16. Изменение компонента представления в файле _ Layout. cshtml < ! DOCTYPE htrnl> <htrnl> <head> <meta name="viewport " content="width=device - width" /> <title>@ViewBag.Title</title> <link asp-href-include=" liЬ/b o otstrap/dist/ css/* .min. css" rel=" stylesheet" /> </head> <body class= "panel-body"> <div class= "bg-pr irnary panel-body" > <div class="row"> <div class="col-xs-7"><hl>Products</hl></div> <div class="col-xs-5">
690 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC @awai t Component .InvokeAsync( "CitySummary") </div> </di v> </div> <div class= "panel - bod y " >@RenderBody()</di v> </bod y> </html> Понятие результатов компонентов представлений Возможность вставки в родительское представление простых строковых значений не особенно полезна, но к счастью компоненты представлений способны на гораздо большее. Более сложных эффектов можно достичь, заставив метод I nvoke () возвра­ щать объект, который реализует интерфейс I ViewComponen tResul t . Доступны три встроенных класса , реализующие интерфейс IV iewComp onentResul t , которые опи­ саны в табл . 22.3 , наряду с удобными методами для создания их экземпляров, предо­ ставляемыми базовым классом ViewComponen t. Использование каждого типа резуль­ тата рассматривается в последующих разделах. На заметку! В случае применения компонентов представлений РОСО создавать экземпля­ ры классов результатов можно напрямую , хотя работать с ними может быть неудобно , т.к. они имеют сложные аргументы конструкторов, которые удобные методы из класса ViewCo mponen t предоставляют самостоятельно. Таблица 22.З. Встроенные классы реализации IViewCornponentResul t Имя ViewViewComponentResult Co n tentVi ewComponent Result Ht mlContentVi ewComponentResult Описание Этот класс используется для указания представ­ ления Razor с дополнительными данными модели представления . Экземпляры этого класса создаются с применением метода View () Этот класс используется для указания текстового результата, который будет безопасно закодирован с целью включения в НТМL-документ. Экземпляры этого класса создаются с применением метода Conten t () Этот класс используется для указания фрагмента НТМL-разметки , которая будет включена в НТМL­ документ без добавочного кодирования . Для со­ здания такого типа результата методы в классе Vi ewComponent не предусмотрены Два типа результатов обрабатываются специа.тiьным образом . Если компонент представления возвращает объект string , тогда он исполь зуется для создания объ­ екта Con tentVi ewCompo n entRe s ul t , на который полагались предшествующие при­ меры. Если компонент представления возвращает объект реализации IHtmlContent, то он применяется для создания объекта Html Conten tViewComponentResul t.
Глава 22. Компоненты представлений 691 Возвращение частичного представления Самым полезным ответом является неуклюже именованный объект ViewViewCornponentResult, который сообщает механизму Razor о необходимости ви­ зуализации частичного представления и включения результата в родительское представ­ ление. Базовый класс ViewCornponent предлагает метод View () для создания объектов ViewViewCornponentResul t, который имеет четыре доступных версии (табл. 22.4). Таблица 22.4. Методы ViewComponent. View () Имя View() View(model) View(viewNarne) View(viewName , rnodel) Описание В случае использования этого метода выбирается стандартное пред­ ставление для компонента представления, а модель представления не указывается В случае применения этого метода выбирается стандартное представление, а указанный объект используется в качестве модели представления В случае применения этого метода выбирается указанное представле­ ние, а модель представления не предоставляется В случае использования этого метода выбирается указанное представле­ ние, а заданный объект применяется в качестве модели представления Описанные в табл. 22.4 методы соответствуют методам, которые предоставляет базовый класс Controller, и используются аналогичным образом. Добавьте в папку Models файл 1\ласса по имени Ci tyViewModel. cs и определите в нем модель пред­ ставления, как показано в листинге 22. 17 . Листинг 22.17 . Содержимое файла Ci tyViewModel. cs из папки Models namespace UsingViewComponents.Models puЫic class CityViewModel { puЬlic int Cities { get ; set; } puЫic int Population { get; set ; В листинге 22.18 приведен модифицированный метод Invoke () компонента пред­ ставления CitySurnmary, который теперь применяет метод View () для выбора час­ тичного пр едставления и передачи ему данных представл ения с использованием объ­ екта Ci tyViewModel. Листинг 22.18. Выбор частичного представления в файле Ci tySummary. cs using System . Linq ; using Microsoft.AspNetCore . Mvc; using UsingViewComponents . Models ; namespace UsingViewComponent s.Components puЫic class CitySurnrnary : ViewCornponent { private ICityRepository repository; puЫic CitySurnrnary(ICityRepository repo) repository = repo ;
692 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic IViewComponentResult Invoke() { return View(new CityViewModel{ Cities = repository.Cities.Count(), Population = repository.Cities.Sum(c => c.Population) }); Выбор частичного представления в компоненте представления похож на выбор представл ения в контроллере, но с двумя важными отличиями: механизм Razor ищет представления в других местоположениях и применяет другое имя представления , если оно не указано . Поскольку для компонента представления частичное представление не создава­ лось, после запуска приложения вы увидите сообщение об ошибке, которое пок аз ы­ вает, какие файлы искал механизм Razor: • /Views/Home/Components/CitySumrnary/Default . cshtml • /Views/Shared/Components/CitySwтunary/De f ault . cshtml Если имя не указано, тогда Razor ищет файл Defaul t . cshtml . В поисках частич­ ного пр едставления механизм Razor просматрива ет два местоположения. П е рво е мес­ тоположение учитывает имя контроллера, обрабатывающего НТТР-з апрос, что позво­ ляет каждому контроллеру иметь собственное пр едстав л ение. Второе местоположение ра зделяется м ежду всеми контроллерами. Совет. Обратите внимание, что разделяемые частичные представления по-прежнему распозна ­ ются компонентом представления , т.е. компоненты представления не разделяют частичные представления . Такое поведение можно переопределить, включая путь в имя представления при вызове метода View (); таким образом, вызов View ( " Views/Shared/Components/ Common/Defaul t . html " ) переопределяет нормальные местоположения для поиска. Чтобы завершить пример , со здайте папку Views/Home/Components/Ci tySummary и добавьте в нее новый файл по имени Defaul t . cshtml , поместив в нег о разметку и з листинга 22.19. Листинг 22.19. Содержимое файла Defaul t. cshtml из папки Views/Home/Components/CitySummary @mode l CityViewModel <tаЫе c l ass= " taЫe taЬle - condensed taЬle - bordered " > <tr> <td>Cities : </td> <td class= " text-right " > @Model . Cities </td> </tr> <tr> <td>Popul ation:</td> <td class= " text -right " > @Model . Popul ation . ToString("# , ### " ) </td> </tr> </tаЫе>
Глава 22 . Компоненты представлений 693 Частичные представления для компонентов представлений работают таким же способом, как и для контроллеров. В данном случае было создано строго типизиро­ ванное представление, которое ожидает объекта CityViewModel и отображает зна­ чения е го свойств Cities и Population в таблице (рис. 22.3). С) Products Х !~ .с[@1~57584 -·- -· -- ----------- --- · Рис . 22.3 . Визуализация представления с использованием компонента представления Возвращение фрагментов НТМL·разметки Класс ContentViewComponentResu l t применяется для включения фрагмен­ тов НТМL -разме тки в родите льское представление, не используя представл ение. Экземпляры класса ContentViewComponentResul t создаются с применением ме­ тода Content () ,унаследованного от базового класса ViewComponent, который при­ нимает значе ние string. Использование метода Content () демонстрируется в лис­ тинге 22 .20. В дополнение к методу Content () возвращать string способен также метод Invoke (),и МVС будет автоматически преобразовывать это значение string в объект ContentViewComponentResult . Листинг 22 .20. Применение метода Content () в файле Ci tySummary. cs using System . Linq ; using Microsoft . AspNetCore . Mvc ; using UsingViewComponents . Mode l s ; namespace UsingViewComponents . Components puЫic class CitySumrnary : ViewComponent { private ICityRepository repository ; puЫi c CitySumrnary(ICityRepository repo) repository = repo ; puЫic IViewCornponentResult Invoke() { return Content( " This is а <h3>< i >stri ng </ i ></h3> " ) ;
694 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Полученная методом Content () строка кодируется. чтобы стать безопасной для включения в НТМL-докум ент. Это особенно важно при работе с содержимым, которое было предоставлено пользователями или внешними системами, т.к. предотвращает внедрение JavaScript-coдepжимoгo в НТМL-разметку, генерируемую приложением. В этом примере строка, которая передается методу Content (), содержит базовые НТМL-дескрипторы, и после запуска приложения вы увидите, что они были безопас­ но закодированы (рис. 22.4). ~ - С ~ localh_ost:S7584 ----- ·---...-----~-----"...- -----·--"'-------··----------------·· Name ~ Price Рис. 22.4 . Возвращение закодированного фрагмента НТМL-разметки с использованием компонента представления Взглянув на НТМL-разметку, выпускаемую компонентом представления , можно заметить, что символы угловых скобок бьши заменены так, что браузер не интерпре­ тирует содержимое как НТМL-элементы: <d iv class= " col-xs-5 " > This is а &lt;hЗ&gt;&lt;i&gt;string&lt;/i&gt;&lt;/hЗ&gt; </div> Кодировать содержимое не понадобится, если вы доверяете источнюtу и хотите его интерпретировать как НТМL-разметку. Метод Content () всегда кодирует свой ар­ гумент. поэтому вы должны создать объект HtmlContentViewComponentResult на­ прямую и передать его конструктору объект HtmlString, представляющий строку, о которой известно, что ее безопасно отображать, либо потому, что она поступила из надежного источника. либо из-за того, что вы уверены в том, что она уже закодиро­ вана (листинг 22.21). Листинг 22.21 . Возвращение фрагмента надежной НТМL-разметки в файле Ci tySummary. cs using System .Linq; using Microsoft . AspNetCore.Mvc ; using UsingViewComponents.Models; using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Html; namespace UsingViewComponents.Components { puЬlic class CitySummary : ViewComponent {
Глава 22. Компоненты представлений 695 private ICityRepository repository; puЫic CitySummary(ICityRepository repo) repository = repo; puЫic IViewComponentResult Invoke() { return new HtmlContentViewComponentResult( new HtmlString("This is а <hЗ><i>string</i></hЗ>")); Такой прием должен применяться осторожно и только с источниками содержи­ мого, которые не могут быть подделаны и выполняют собственное кодирование. Запустив приложение, вы увидите, что угловые скобки были включены в родитель­ ское представление без модификации, что позволяет браузеру интерпретировать вы­ вод компонента представления как НТМL-элементы (рис. 22.5). Получение данных контекста Детали о текущем запросе и родительском представлении компонент представле­ ния получает через свойства класса ViewComponentContext, наиболее полезные из которых описаны в табл. 22.5 . Таблица 22.5 . Полезные свойства класса ViewComponentContext Имя Arguments Описание Это свойство возвращает словарь аргументов, предостав­ ляемых представлением, который можно также получить через метод Invoke () HtmlEncoder Это свойство возвращает объект HtmlEncoder , который можно применять для безопасного кодирования фрагмен­ тов НТМL-разметки ViewComponentDescriptor Это свойство возвращает объект ViewComponentDescriptor, который предоставляет описание компонента представления ViewContext Это свойство возвращает объект ViewContext из родительского представления. Возможности класса ViewContext обсуждались в главе 21 ViewData Это свойство возвращает объект ViewDataDictionary, который открывает доступ к данным представления, пред­ назначенным для компонента представления Базовый класс ViewComponent предлагает набор удобных свойств, которые об­ легчают доступ к специфической информации контекста (табл. 22.6).
696 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC [j Products х Price -....~~/-.•-" ..... -. Рис. 22.5 . Возвращение фрагмента незакодированной НТМL-разметки с использованием компонента представления Таблица 22.6 . Удобные свойства класса ViewComponent Имя Описание ViewComponentContext Это свойство возвращает объект ViewComponentContext HttpContext Это свойство возвращает объект HttpContext, который описы­ вает текущий запрос и подготавливаемый ответ Request Это свойство возвращает объект HttpReques t, который описы­ вает текущий НТТР-запрос User Это свойство возвращает объект реализации IPrincipal, кото­ рый описывает текущего пользователя (глава 28) RouteData Это свойство возвращает объект RouteData, который описывает данные маршрутизации для текущего запроса (глава 15) ViewBag Это свойство возвращает динамичес кий объе кт ViewBag, кото­ рый можно использовать для передачи данных между компонен­ том представления и представлением ModelState ViewContext ViewData Url Это свойство возвращает объект ModelStateDictionary, кото­ рый предоставляет детали процесса привязки моделей (глава 26) Это свойство возвращает объект ViewContext, который был пе­ редан родительскому представлению (глава 21) Это свойство возвращает объект ViewDataDictionary, кото­ рый обеспечивает доступ к данным представления для компонен­ та представления Это свойство возвращает объект реализации IUrlHelper, который можно применять для генерации URL, как объяснялось в главе 15 Данные контекста могут использоваться любым спо собом, который помогает ком­ поненту представления выполнять свою работу, включая варьирование метода вы­ бора данных или визуализацию другого содержимого либо представлений. В листин­ ге 22.22 данные маршрутизации применяются для суж ения выборки объектов Ci ty.
Глава 22 . Компоненты представлений 697 Листинг 22.22. И спользование данных контекста в файле CitySummary. cs using System . Linq ; using Mic r osoft . AspNetCore . Mvc ; using UsingViewComponents . Models ; using Microsoft . AspNetCore . Mvc . Vi ewComponents ; using Microsoft . AspNetCo re . Mvc . Rendering ; n amespace UsingViewComponents . Co mponents { puЫic class CitySummary : ViewComp o n en t { pr i vate I CityRepos i tory repository ; puЫic Ci tySummary( I CityRepository repo} repository = repo ; puЫic IViewCompon ent Res ult I nvoke () { string target = RouteData.Values["id"] аз string; var cities = repository.Cities . Where (city => target == null 11 string.Compare(city.Country, target, true) О); return View(new CityViewModel{ Cities = cities.Count(), Population = cities.Sum(c => c.Population) }); Брау зе р применя ет сегмент id из м аршрут а для указания страны , которая испо ль­ зуется LINQ при фильтрации объектов в хранилище. Запустив приложение и запро­ сив стандартный URL , вы увидите, что отображается информация по всем городам. Сузить выборку можно путем запрашивания URL вроде / Home/ I ndex/ USA , который огр аничит выбор городами в США (рис. 22.6) . [':) Product s х Рис. 22.6 . Применение данных контекста в компоненте представления
698 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC получение данных контекста из родительского представления с использованием аргументов Родительски е представления могут предоставлять дополнительные данные кон­ текста в виде аргументов для выражения @await Cornponent. Invoke . Это с р едство можно применять для получения данных из модели родительского представления или для сообщения инструкции о типе содержимого, которое должен выдавать компонент пр едставления. Чтобы посмотреть на него в работе, создайте в папке Views/Horne/ Cornponent/Ci tySurnrnary файл представления по имени Ci tyList . cshtml и добавь­ те в него разметку из листинга 22.23. Листинг 22.23. Содержимое файла CityList. cshtml из папки Views/Home/Component/CitySummary @model IEnumeraЫe<City> <tаЫе class= " ta Ы e taЫe -c ondens ed taЬle-bordered"> @foreach (var city in Model) ( <tr> <td>@city . Name</td> <td c l ass= "text- right "> @city.Population.ToString( " # , ### " ) </td> </tr> <tr> <th>Total : </th> <td class= "t ext - right " > @Model . Sum(p => p . Popul ation) .ToString( " # , ### ") </td> </tr> </tаЫе> Определение второго представления позволит компоненту представления произ­ водить выбор между ними на основе аргумента , добавленного к методу Invoke (), как показано в листинге 22.24 . Листинг 22.24 . Выбор представления в файле Ci tySummary. cs using System . Linq ; using Microsoft . AspNetCore.Mvc ; using Usi ngVi ewComponents .Mod e l s ; using Microsoft .AspNe t Core . Mvc . ViewComponents ; using Microsoft . AspNetCore.Mvc.Rendering ; namespace UsingV iewComponents . Components { puЫic c l ass CityS urnmary : ViewCornponent { private ICityRepository repository; puЫic CitySummary(ICityRepository repo) repositor y = repo; puЬlic IViewComponentResul t Invoke (bool showList) if (showList) { return View("CityList", repository.Cities);
Глава 22. Компоненты представлений 699 else { return View(new CityViewModel Cities = repository.Cities.Count(), Population = repository.Cities.Sum(c => c.Population) }); Если аргумент showList метода Invoke () равен true, тогда компонент пред­ ставления выбирает CityList и передает все объекты City из хранилища как мо­ дель представления. Если аргумент showList равен fals e, тогда выбирается стан­ дартное представление с указанием объекта Ci tyViewModel в качестве модели представления. Финальный шаг связан с предоставлением данных контекста, когда компо­ нент представления применяется в родительском представлении, для чего методу Invoke () передается анонимный объект (листинг 22.25). Листинг 22.25 . Предоставление данных контекста во время применения компонента представления в файле _ Layou t . cshtml < ! DOCTYPE html> <html> <head> <meta name= " viewpor t" content=" width=dev ice - width " /> <title>@ViewBag . Title</title> <link asp-href-include= " liЬ/bo otst rap/dist/ css/* .min. css " rel=" stylesheet" /> </head> <body class= " panel - body " > <div class="bg - primary panel - body " > <div class= " row " > <div class="col-xs - 7 "><hl>Products</hl ></div> <div class= " col -xs -5" > @await Component .InvokeAsync ("CitySummary ", new { showList = true }) </div> </div> </div> <div class= " panel-body " >@RenderBody()</div> </body> </html > После запуска приложения компонент представления получит значение, указываемое родительским представл ением , и отреагирует соответствующим образом (рис . 22.7).
700 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC D Pr oducts Х ~ " С [ ф local~os~~S75~/Hoine/lndex/US~-·---·· -~ ; 1 -·-------·-- ---- ___4_, __ _ Рис. 22.7. Предоставление данных контекста компоненту представления Модульное тестирование компонентов представлений Компоненты представлений следуют общему подходу MVC, предусматривающему отделе­ ние логики выбора и обработки данных модели от разметки представления , которая их фор­ матирует и отображает, что облегчает проведение модульного тестирования. Вот пример модульного теста для класса CitySummary из рассмотренного приложения: using System . Collections.Generic ; using Microsoft . AspNetCore.Mvc.ViewComponents ; using Moq ; using UsingViewComponents . Models ; using UsingViewComponents.Components; using Xunit; namespace UsingViewComponents .Tests { puЫic class SummaryViewComponentTests [Fact] puЫic void TestSummary() { 11 Организация var mockRepository = new Moc k <ICityRepos i tory>(); mockRepository.SetupGet(m => m.Cities) . Returns(new List<City> { new City { Population 100 } , new City { Population 20000 ) , new City { Populat i on 1000000 } , new City ( Populat ion 500000 } )); var viewComponent = new CitySummary(mockRepository.Object) ;
11 Действие ViewViewComponentResult result Глава 22 . Компоненты представлений 701 = viewComponent . Invoke(false) as ViewViewComponentResult ; 11 Утверждение Assert . IsType(typeof(CityViewModel) , result . ViewData . Model) ; Assert .Equal(4 , ((CityViewModel)result . ViewData . Model) . Cities) ; Assert . Equal(l520100 , ( (CityViewModel)result . ViewData . Model) . Population) ; Для организации теста создается имитированное хранилище, которое передается конструк­ тору класса CitySurnmary, чтобы создать экземпляр компонента представления. В разде­ ле действия теста вызывается метод Invoke (),который предоставляет объект результата. Компонент представления выбирает представление Razor, поэтому результат приводится к типу ViewViewComponentResul t и затем через предлагаемое им свойство ViewDa ta . Model осуществляется доступ к объекту модели представления. В разделе утверждения теста проверяется тип данных модели представления и содержащиеся в ни х значения . Создание асинхронных компонентов представлений Все рассмотренные до сих пор примеры были синхронными 1<омпонентами пред­ ставлений, которые легко опознать по тому, что они определяли метод Invoke (). Если компонент представления полагается на асинхронные АРI-интерфейсы, то мож­ но создать асинхронный компонент представления, определив метод InvokeAsync (), который возвращает объект Task. Когда механизм Razor получает объект Task из ме­ тода InvokeAsync () , он будет ожидать его завершения и затем вставит результат в главное представление. Для подготовки настоящего примера в проект понадобится добавить пю<ет (листинг 22.26). Листинг 22.26. Добавление пакета в файле project. json " dependencies ": { "Microsoft .NETCore .App ": "version": "1.0.О", " type ": "platform " )1 " Microsoft . AspNetCore . Diagnostics " : "1. О. О ", " Microsoft . AspNe t Core . Server . IISintegration ": "1. 0 . 0 ", " Microsoft . AspNetCore . Server . Kestrel ": " 1 . О . О ", " Microsoft.Extensions . Logging . Consol e": " 1 . 0 . 0 ", "Microsoft.AspNetCore .Mvc" : "1.О.О", " Microsoft. AspNetCore . StaticFiles ": " 1. О. О ", " Microsoft . AspNetCore . Razor . Tools" : }1 " version ": "l. O. O- preview2-final ", " type " : "build" "System.Net.Http": "4.1.0" )1
702 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Пакет System. Net . Http предоставляет АРl-интерфейс для выполнения асинхрон ­ ных НТГР-запросов, который будет использоваться для отправки запросов к веб -сайту Apress.com. Добавьте в папку Comp onents файл класса по имени PageSize . cs, с о­ держимое которого показано в листинге 22.27 . Листинг 22.27. Содержимое файла PageSize. cs из папки Components using System.Net .Http; using System. Th r e ading. Tasks ; using Micr osoft .AspNe tCore . Mvc ; namespace UsingViewComponents . Co mponents puЬlic class PageSi ze : Vi ewComponent { puЫic async Task<IViewCompone nt Result> I nvokeAsync() { HttpCli ent client = new HttpClient() ; Htt pRe spons eMe ssage r esponse = a wa it cli ent.GetAsync( "ht tp: //apres s. com" ) ; return View(response . Co n tent .Headers . Content Length) ; В методе I nvokeAsync () применяются ключевые слова asyn c и awai t , описан­ ные в главе 4 , для потребления асинхронного АРl-интерфейса, пр едлагаемого 1шас­ сом HttpClient, и получения длины содержимого , возвращаемого в результате от­ правки з апроса GET веб-сайту Apress.com . Длина п ер едается методу View () ,ко торы й выбирает стандартное частично е представление , ассоциированное с компон е нтом представления. Чтобы создать это представление, добавьте в проект папку Views / Home / Componen ts / PageS i z e и помес тите в нее файл представления по имени Default . cshtml с содер­ жимым, приведенным в листинге 22.28. Листинг 22.28. Содержимое файла Defaul t. cshtml из папки Views/Home/Components/PageSize @model l o ng <div class=" panel-body bg- i nfo ">Page si ze : @Model</di v> Осталось лишь использовать компонент, что и делается в файле _Layout. cshtml (листинг 22.29). Листинг 22.29. Применение асинхронного компонента представления в файле _Layout. cshtml < ! DOCTYP E html> <html > <head> <meta n ame= " viewport " content= " width=device - width " /> <tit l e >@ViewBag.T i t l e</title > <li nk asp-href-include="liЬ/bootstrap/dist/css/* .min. css " rel =" st yl esheet" /> </head>
Глава 22. Компоненты представлений 703 <body cl ass= "panel-body"> <di v cl ass=" bg-pri rnary panel-body"> <div cl ass=" row "> <div cl ass=" col- xs- 7 "><hl>Product s</hl></div> <div cl ass=" col-xs-5"> @awai t Cornponent . I nvokeAs ync ( "Ci t ySurnrna ry", new { showList = t rue }) </di v> </div> </di v> <div class= "pane l- body " >@Re n d er Body()</ di v> @await Component. InvokeAsync ("PageSize") </body> </htrnl> Запустив приложение, вы увидите, что браузер отображает новое добавление к со­ держимому (рис. 22.8). Выводимое число может измениться, когда вы запустите при­ мер приложения , т.к. в Apress часто обновляют свой веб-сайт. !1 J i: t;occer ball 1&.~u l.-... -·-- -----·---·---------------·-------------......! Рис. 22.8 . Создание асинхронного компонента представления Создание гибридных компонентов контроллеров/представлений Компоненты представлений часто предоставляют сводку или краткую характерис­ тику функциональности, которая поддерживается контроллером. Например, компо­ не нты представлений, отображающие сводку по корзине для покупок, нередко содер ­ жат ссылку, нацеленную на контроллер , который выводит подробный список товаров в корзине и может исполь зоваться для зав е ршения покупок и перехода к оплате . В такой ситуации можно создать класс, являющийся контроллером и компонентом представления, который позволяет сгруппировать вместе связанную фующиональ­ ность и сократить дублирование кода. Добавьте в папку Contro lle r s файл класса по имени Ci tyController . c s и определите в нем контроллер, как показано в лис­ тинге 22.30 .
704 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 22.30. Содержимое файла CityController. cs из папки Controllers usi ng Syst em.Coll ections .Generic; using Mi crosoft . AspNe t Core . Mvc ; using Mi crosoft . As p NetCore . Mvc . Vi ewComponents ; using Mi crosoft .AspNetCore . Mvc . Vi ewFe a tures ; us i ng Us ingViewCompon ents . Model s ; n amespace UsingVi ewComponents . Cont r ollers [Vi e wComp o nent (Name = " ComboComponent")] puЬlic class CityCont r oller : Cont r oller private ICityRepository repository; puЬlic CityControl le r ( I Ci tyRe pos itor y repo) repository = repo; puЬlic ViewResult Create() => View(); [ HttpPost ] puЫic I Action Res u l t Create(City n ewC ity) repository.AddC ity(newC i ty) ; return RedirectToAction ( " Ind e x", " Home " ) ; p u Ьli c IV iewComponentRes u lt Invoke() => n ew Vi ewVi ewCompo n e n t Result() ViewData = new ViewDataDictionary<IEnumeraЬle<City>>(ViewData, r epository . Cities) }; Атрибут Vi ewCompo nen t применяется к классам , не унаследованным от базово ­ го класса Vi ewCompo ne n t и не им е ющим в своих именах суффикса ViewCompone n t, т.е. нормальный процесс обнаружения не сможет отнести их к категории компонен­ тов представлений. Свойство Na me устанавлива ет имя , по которому на класс м ож ­ но ссылаться во время его исполь з ования в выр аж ении @Component . Invoke внут­ ри родительского представления. В данном примере свойство Name применяется для установки имени части класса, относящейся к компоненту представл е ния , в ComboComp onent . Это имя будет использоваться для обращения к компоненту пр ед­ ставления и поиска его представл е ний. Поскольку гибридные классы не наследуются от базового класса ViewCompone n t, они не имеют доступа к удобным методам для создания объектов реал и за ­ ции I Vi e wCompon e ntRe s ul t , что означает н е обходимость в создании объ е кта Vi e wv iewComponen tResul t напрямую, как делалось бы в компоненте представления РОСО. Создание гибридных представлений IИбридный класс требует двух наборов представлений: того, который визуализи­ руется в случае применения класса как контроллера. и того , который визуализирует­ ся при использовании класса как компонента представления. Создайте папку Views/ Ci t y и добавьте в нее файл представления по имени Creat e . csht ml с сод е ржимым из листинга 22.31.
Глава 22 . Компоненты представлений 705 Листинг 22.31. Содержимое файла Create. cshtml из папки Views/City @model City @{ ViewData ["T itle "J Layout = " _ Layout "; " Create City "; <form method= " post " asp - action= " Create " > <div class= " form- group "> <label asp-for= " Name " >Name : </lab el> <input class= " form - control " asp - for= " Name " /> </div> <div class= " form - group " > <label asp - for= " Country " >Co u ntry : </label> <input c l ass= " form-control " asp - for= " Country " /> </div> <div class= " form- group"> <label asp - for= " Pop u lation " >Popula ti o n: </ l abel> <input class= " form - control " asp-for= " Population " /> </div> <button type= " submit " class= " Ьt n ьt n- p r i m ary " >Create</b u tton> <а class="Ьtn Ьtn-default" asp- controller = " Home " asp - action= " Index " > Cancel </а> </form> Это пр едставление выводит простую форму для создания новых объектов Ci ty. Щелчок на кнопк е Create (Создать) приводит к отправк е запроса POST к дей ствию Create контроллера City, а на кнопке Cancel (Отмена) - к отправке запроса GET к действию Index контролл ера Ноте . Создайте пап ку Vi e ws/S h ared/Compon en ts/ComЬoCo mpone n t и добавьте в не е файл представления по имени Defaul t . csh trnl, содержимое которого показано в листинге 22.32. Частичное представл ение помещено в папку Views/Shared , потому что она будет применяться для поиска представлений контроллером, в чьем пр ед­ с т авле нии исполь зуется компон е нт пр едставл ения. имя которого включа ется в путь. Листинг 22.32. Содержимое файла Defaul t. cshtml из папки Views/Shared/Components/ComЬoComponent @mode l IEnumeraЫe<City> <tаЫе class= " taЫe taЬle - condensed taЬle - bordered " > <tr> <td>B i ggest City : </td> <td> @Model . Orde r ByDescending(c => c .Po pu lation ) . First() . Name </td> </tr> </tаЫе> <а class="Ьtn Ьtn-sm Ьtn-info" asp- cont r oller= "City" asp- action= " Create"> Create City </а>
706 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Частичное представление Defaul t. cshtml получает последовательность объек­ тов Ci ty и сортирует ее с помощью LINQ, чтобы выбрать объект с наибольшим зна­ чением Popu la tion. Кроме того, определен якорный элемент, стилизованный под кнопку, который нацелен на действие Create контроллера City. Совет. Обратите внимание на явное указание контроллера Ci ty для элемента а в лис­ тинге 22.32 . Дело в том, что URL генерируются с применением данны х контекста, пре­ доставляемых родительским представлением; это значит, что запрос обрабатывается стандартным контроллером, а не контроллером, который является также компонентом представления. Если опустить атрибут asp - controller, то ссылка будет нацелена на действие Create контроллера Home . Применение гибридного класса Финальный шаг заключается в применении гибридного класса 1\ак компонента представления в разделяемой компоновке с использованием имени, которое было ука­ зано посредством атрибута ViewCornpo n ent (листинг 22.33) . Листинг 22.33 . Применение гибридного класса в файле_Layout. cshtml <!DOCTYPE html> <html> <head> <meta name= "viewport " content= " width=device -w idth " /> <title>@ViewBag . Title</title> <link asp-href -i nclude="liЬ/bootstrap/dist/css/* . min.css" rel= " stylesheet " /> </head> <body class= " panel - body " > <div class= "bg-primary panel-body"> <div class="row " > <div class= " col - xs - 7"><hl>Products</hl></div> <div class="col-xs - 5 "> @awai t Component . InvokeAsync ( "ComЬoComponent") </div> </div> </div> <div class =" panel - body " >@RenderBody()</div> </body> </html> Результатом будет компонент представления, оснащенный собственным интегри­ рованным контроллером (или, если хотите, контроллер, который имеет собственный интегрированный компонент представления). Запустив приложение, вы увидите, что в качестве самого густонаселенного города указан Лондон (L ondon). Щелкните на кнопке Create City (Со здать город). чтобы отобразить форму, которая позволит до­ бавить новый объект Ci ty в приложение. Заполните и отправьте форму , после чего контроллер получит данные, обновит хранилище и перенаправит браузер на стандар­ тный URL приложения. Если вы добавили объект Ci ty, население которого превы­ шает показ атели населения других городов, имеющихся в хранилище, тогда вывод из компонента представления изменится, как пок азано на рис. 22.9 .
х с [~ ~~~!~!ё~~~~~~--=-~-==~=--;=ю. -~-! Norne: Bei1lng Counlry: Chlna Population : Nam e / Kayak 1 Lile/ackel " \ Can~~ , Socce r ba!I - -.- -------- ----- -· ---·-- -{ 111 Глава 22 . Комnоненты nредставлений 707 Price 275 48. 95 19 .50 1 [__ ·------ -- -- - ----- ---- -- - _J Рис . 22.9 . Использование гибридного компонента контроллера/представления Резюме В этой главе было дано введение в компоненты представлений, которые являют­ ся новым средством ASP.NET Core MVC и заменяют собой средство дочерних дейс­ твий из предыдущих версий MVC . Вы узнали, каким образом создавать компоненты представлений РОСО и как применять базовый класс ViewComponent . Были проде­ монстрированы три разных типа результатов, которые могут выпускать компонен­ ты представлений , в том числе выбор частичных представлений для их включения в родит ел ьско е представление. В завершение главы было показано , как добавить фун ­ кциональность компонентов представлений в класс контроллера, чтобы сократить дублировани е кода и упростить приложение. В следующей главе рассматриваются дескрипторные вспомогательные классы , которые используются для трансформации НТМL-элементов в представлениях.
ГЛАВА 23 Дескрипторные вспомогательные классы д ескрипторные вспомогательные классы - это новое средство, появившееся в ASP.NET Co re MVC. Они являются классами , которые трансформируют НТМL­ элеме нты в представлении. Распространенные случаи использования дескрипторных вспомогательных классов включают генериров ание URL для форм с применен ием конфигурации маршрутизации приложения, обеспечение согласованной стилизации элементов специфического типа и замена специальных СОI{ращающих элементов хо­ довыми фрагментами содержимого. В настоящей главе будет показано, как работают дескрипторные вспомогательные классы и каким обра зом создавать и использовать специальные деск рипторные вспомогательные классы. В главе 24 будут описаны встроенные деск рипторны е вс по могател ьные классы, которые поддерживают НТМL­ формы, а в главе 25 - другие встроенные дескрипторные вспомогательные классы, предоставляемые инфраструктурой MVC. В табл . 23.1 приведена сводка, позволяю­ щая поместить дескриптор ные вспом огательные классы в контекст. Таблица 23. 1. Помещение дескрипторных вспомогательных классов в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие-то скры - тые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией MVC 5? Ответ Дескрипторные вспомогательные классы - это классы , которые мани­ пулируют НТМL- элементами с целью их изменения каким - либо обра­ зом, дополнения добавочным содержимым или полной замены новым содер жим ым Дескрип торные вспомогательные классы позволяют генерировать либо трансформировать содер жимое представления с использовани­ ем логики С#, гарантируя, что отправляемая браузеру НТМL-разметка отражает состояние приложения Элементы HTML , к которым применяются дескрипторные вспомогатель­ ные классы, выбираются на основе име ни класса или за счет использова­ ния атрибута HTMLTargetElement . Когда представление визуализиру­ ется , эле м енты трансформ и руются дескрипторными вспомогательными классами и включаются в НТМL-разметку, отправляемую клиенту Довольно легко увлечься и генерировать с применением дескриптор­ ных вспомогательны х классов сложные разделы НТМL-разметки, что намного проще достигать с помощью компонентов представлений, как объяснялось в главе 22 Вы не обязаны использовать дескрипторные вспомогательные классы , но они облегчают генерацию сложной НТМL-разметки в приложе ниях MVC Дескрипторные вспомогательные классы являются новым добавлени­ ем в ASP.NET Core MVC и заменяют собой функциональность , которую предлагали вспомогательные методы HTML
Глава 23. Дескрипторные вспомогательные классы 709 В табл. 23.2 прив едена сводка для настоящей главы. Таблица 23.2 . Сводка по главе Задача Трансформация НТМL-элемента Управление областью дейс­ твия дескрипторного вспомо- гательного класса Решение Создайте дескрипторный вспомогательный класс и зарегистрируйте его с примене­ нием выражения @addTagHelper в пред­ ставлении или в файле импортирования представлений Используйте атрибут Html TargetElement Листинг 23 .1-23.13 23 . 14-23.18 Поддержка сокращающего элемента Применяйте объект TagHe l perOutput для 23.19, 23.20 генерирования заменяющих элементов Вставка содержимого вок ­ руг или внутрь целевого элемента Используйте предоставляемые классом 23.21 -23.24 TagH elper Ou t put свойства, имена которых начинаются на Pre и Pos t Получение данных контекста в дескрипторном вспомога­ тельном классе Декорируйте свойство атрибутами 23.25, 23.26 Получение доступа к модели представления ViewContext и Html Att ributeNo t Bound Применяйте свойство ModelExpression Согласование дескрипторных Используйте свойство вспомогательных классов TagHelperCon text . Items Подавление элемента Применяйте метод Supp res s Output () Подготовка проекта дл я прим ера 23.27, 23 .28 23.29 , 23.30 23.31, 23.32 Создайте новый проект типа Empty (Пустой) по имени Ci t i e s с использова ­ ни ем шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependenc i es файла project . json и н астройте инструментарий Razor в разд еле tools , как показано в ли стинге 23.1. Ра зделы , которые н е нужны для данной главы, понадобится удалить . Листинг 23.1 . Добавление пакетов в файле project. j son " dependencies ": { " Microsoft . NETCore . Ap p": " version ": 11 1.0 .0 11 , " type ": " platform " }' " Microsoft . AspNetCore . Diagnostics ": "1 .О.О", " Microsoft . AspNetCore . Server . IISintegration": " 1 . 0 . 0 ", "Microsoft .AspNetCore.Server . Ke strel ": " 1 . 0 . 0 ", "Mi crosoft.Extensions.Loggi ng.Console": "1 .0 .0", "Microsoft.AspNetCore.Mvc": 11 1.0.0 11 , "Microsoft.AspNetCore.StaticFi1es": 11 1.0.0 11 ,
71 О Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC }/ "Microsoft.AspNetCore.Razor.Tools": 11 version": 11 1. О. 0-preview2-final. 11 , "type": "build" 11 t ools 11 : " Microsoft . AspNetCore .Server.IISintegration . Tools ": "l. 0.0 - preview2 - final ", "Microsoft.AspNetCore.Razor.Tools 11 : 11 1.0 .0 -preview2-final" }/ 11 frameworks 11 : { 11 netcoreappl . 0 11 : 11 imports 11 : [ " dotnetS.6 11 , " portaЬle - net45+win8"] }' " buildOptions 11 : " emi tEntryPoint 11 : true, " preserveCompilationContext 11 : true }/ 11 runtimeOptions 11 : { 11 configProperties 11 : { "S ystem.GC . Server ": true} Создание модели и хранилища Создайте папку Models и добавьте в нее файл класса по имени Ci ty. cs с опреде­ лением из листинга 23.2 . Листинг 23.2. Содержимое файла City. cs из папки Models namespace Cities . Models { puЫic class City { puЫic string Name { get; set; } puЫic string Country { get; set; puЬlic int? Population { get; set ; } Чтобы создать хранилище для объектов City, добавьте в папку Models файл по имени Reposi tory . cs и определите в нем интерфейс и класс реализации, как пока­ зано в листинге 23.3 . Листинг 23.З. Содержимое файла Reposi tory. cs из папки Models using System . Collections.Generic ; namespace Cities.Models { puЫic interface IRepository IEnumeraЫe<City> Cities { get ; void AddCity(City newCity);
Глава 23. Дескрипторные вспомогательные классы 711 puЫic class MemoryRepository : IRepository { private List<City> c i tie s = new List<City> { new City { Name = " London ", Country = " UK ", Population = 8539000} , new City { Name = " New York ", Country = " USA ", Population = 8406000 }, new City { Name = " San Jose", Country = " US A", Population = 998537 }, new City { Name = " Paris ", Country = " France ", Population = 2244000 } }; puЫic IEnumeraЬle<City> Cities => cities ; puЫic void AddCity(City newCity) ci t ies . Add(newCity) ; Создание контроллера, компоновки и представлений В примерах этой главы требуется только один контроллер. Создайте папку Control l ers. добавьте в нее файл класса по имени HorneController . cs и опреде­ лите контролл ер , код которого приведен в листинге 23.4 . Листинг 23.4 . Содержимое файла HomeController. cs из папки Controllers using Microsoft.AspNetCore . Mvc ; using Cities . Models ; namespace Cit i es .C ontro ll ers { puЬlic class HomeController : Controller { private IRepository repository ; puЬlic HomeController(IRepository repo) { repository = repo ; puЫic ViewRe sult Index() => View(repository.Cities) ; puЫic ViewResult Create() => View() ; [HttpPost] puЫic IActionResult Create(City city) repository . AddCity(city) ; return RedirectToAction( " Index " ) ; Контролл е р поддерживает метод действия Index () , выводящий список объектов в хранилище, и пару методов Crea te ( ) , которые позволят пользователю применять форму для создания новых объектов Ci ty, следуя тому же самому подходу, что и при­ меры в предшествующих главах. Представления в данном приложении будут использовать разделяемую компонов­ ку. Создайте папку Views/ Shared и поместите в нее файл по имени_Layout. cshtrnl с разметкой из листинга 23.5 .
712 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC На заметку! Поскольку цель настоящей главы заключается в демонстрации работы дескрип­ торных вспомогательных классов, компоновка и представления в примере приложения реализованы с применением только стандартных НТМL-элементов, которые будут заме­ няться по мере рассмотрения различных дескрипторных вспомогательных классов . Листинг 23.5 . Содержимое файла _Layout. cshtml из папки Views/Shared < ! DOCTYPE html> <html> <head> <meta name= "viewport " content= "width=device - width " /> <title>Cities</title> <link href= " /l i Ь/bootstrap/dist/css/bootstrap . css " rel= " styl esheet " /> </head> <body class= " panel - body " > <div>@RenderBody()</di v> </body> </html> Создайте папку Views/Home и добавьте в нее файл по имени Index.cshtml, со­ держимое которого показано в листинге 23.6 . Листинг 23.6 . Содержимое файла Index. cshtml из папки Views/Home @model IEnumeraЫe<City> @{ Layout = " Layout "; } <tаЫе class= " taЫe taЬle-condensed taЫe-bordered"> <thead class= " bg-primary " > <tr> <th>Name</th> <th>Country</th> <th class= " text -right " >Population</th> </tr> </thead> <tbody> @foreach (var city in Model) <tr> <td>@city .N ame</td> <td>@city . Country</td> <td c l ass= " text-right " >@city. Populat i on? .ToString( " # , ### " )</td> </tr> </tbody> </tаЫе> <а href=" /Home/Create " class= "Ьtn Ьtn-primary">Create</a> В представлении используется последовательность объектов Ci ty для заполн ения таблицы и включается элемент а , нацеленный на URL вида /Home/Create, который с помощью Bootstrap стилизован под кнопку . Чтобы создать второ е предст авл ени е , добавьте в папку Views/Home файл по имени Create.cshtml с разм еткой из лис­ тинга 23.7 .
Глава 23 . Дескриnторные вспомогательные классы 71 З Листинг 23.7 . Содержимое файла Create. cshtml из папки Views/Home @rnodel City @{ Layout = "_L ayo ut"; } <forrn rnethod="post" action= "/Horne/Create" > <div class=" forrn-group " > <label for= " Narne " >Narne : </ l abel> <input class="forrn-control" narne="Narne" /> </div> <div class= " forrn -group " > <label for= " Country " >Country : </label> <inpu t class= "forrn - control" narne="Country" /> </div> <div class= " forrn-group "> <label for= "P opulation " >Popu l ation : </label> <inpu t class= "fo rm-control " name="Population" /> </div> <button type= " subrnit " class= "Ьt n Ьtn-primary">Add</button> <а class= " Ьtn Ьtn - primary " href= " /Home/ Index " >Cancel </a> </forrn> Со зд айте в папке Vi е ws файл импортирования представлений по имени _ Viewimports. cshtml и поместите в него выражение, приведенное в листинге 23.8 . Оно позволит ссылаться на классы из папки Models , не указывая пространство имен. Листинг 23.8 . Содержимое файла_Viewirnports. cshtml из папки Views @using Cities . Models Представления в рассматриваемом примере полагаются на СSS-пакет Bootstrap. Создайте в корневой папке проекта файл bower. j son с применением шаблона эле­ мента Bower Contiguration File (Файл конфигурации Bower) и добавьте пакет Bootstrap в раздел dependencies (листинг 23.9). Листинг 23.9 . Добавление пакета Bootstrap в файле bower. j son "name ": "asp . net", " private ": true , " dependencies " : { "bootstrap": 11 3.3.6" Конфи гурирование приложения Последний подготовительный шаг связан с конфигурированием приложения. как пок азано в листинге 23.10 . Это та же сам ая базовая конфигурация, используемая во всех проектах в данной части книги, к которой добавлена реги ст рации хранилища в виде службы с применением жизненного цикла одиночки.
714 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 23.1 О. Содержимое файла Startup . cs using Micro soft . AspNetCo re.Builder ; using Micro soft . Extensions.Depende n cyinjecti o n ; u s ing Cities . Models; namespace Citie s { puЫic cla s s Startup puЫ i c void Config u reServices (IServiceColl ecti on services) services.AddSingleton<IRepository, MemoryRepository>(); services.AddМvc(); puЫic void Configure(IAppl i ca t ionBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app.UseMvcWithDefaultRoute(); Запустив приложение . вы увидите список объектов City. который хр анилище со­ здает по умолчанию . Щелкните на кнопке Create (Создать), заполните форму и щелк­ ните на кнопке Add (Добавить) ; в хранилище добавится новый объект (рис. 23.1) . D Cities D Citios Х С [ф l; all1ostS21 L- --- -- -- -- --- ~--- г;::.: ------- 1 • с 1~~.1_21 1-------- ---- 1- • 1 1 , London Ne1v York B eijing Nam e: Country : 1 SanJose 1 China 1:, 1 '°;,~:"00 1---- · -----·-------1.L ___ . __ ~ 'сjФ~;smJ----~1:1 ·-----·--=-=--=-=-- , Londo n New Yo rk Sa n Jose Paris Beijing UK USA USA France Ch ina 8,539 ,000 8,406 ,000 998,537 --1-~:_:~_,:J Рис . 23.1. Выполнение примера приложения
Глава 23. Дескрипт ор ные вспомогательные классы 715 Создание дескрипторного вспомогательного класса Как и со многими средствами МVС, пониманию дескрипторных вспомогательных классов лучше всего способствует создание одного такого класса, что позволит вы­ явить особенности их работы и согласование с остальными частями приложения. В приведенных далее разделах вы ознакомитесь с процессом создания и исполь­ зования дескрипторного вспомогательного класса, который будет применять СSS­ классы Bootstrap к элементу button, так что элемент вида: <button type= " submit " bs-button-co lor= " danger " >Add</ bu tton> трансформируется следующим образом: <button type= " submit " class= "btn Ьtn-danger">Add</button> Дескрипторный вспомогательный класс будет распознавать атрибут bs - button- color и использовать его значение для установки атрибута class элемента , от­ правляемого браузеру. Это не особо впечатляющая или сколько-нибудь полезная трансформация, но она формирует основу для объяснения работы дескрипторных вспомогательных классов. Определение дескрипторного вспомогательного класса Дескрипторные вспомогательные классы можно определять гд е угодно в проек­ те, но их удобно держать вместе , потому что в отличие от большинства компонен­ тов МVС перед примен ением они нуждаются в регистрации. Добавьте в проект папку Infrastructure/TagHelpers, в которой будут создаваться дескрипторные вспомо­ гательные классы. Дескрипторные вспомогательные классы являются производными от класса TagHelper , который определен в пространстве имен Microsoft. AspNetCore . Razor . TagHelpers. Чтобы создать дескрипторный вспомогательный класс, добавьте в папку Infrastructure/TagHelpers файл по имени ButtonTagHelper. cs и опре­ делите в нем н:ласс, как показано в листинге 23. l l. Листинг 23.11 . Содержимое файла ButtonTagHelper. cs из папки Infrastructure/TagHelpers using Microsoft . AspNetCore . Razor .T agHe lpers ; namespace Cities .Infrastruc ture.Tag Help ers { puЬlic class ButtonTagHelper : TagHelper { puЫic string BsB uttonColor { get; set; ) puЫic ove rride void Process(TagHelperContext context , TagHelperOutput output) { output.Attributes.SetAttribute( " class ", $ " Ьtn Ьtn-{BsButtonColor)") ; )
716 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC В классе TagHelper определен метод Process (),который переопределяется под­ классами для реализации поведения, трансформирующего элементы. Имя дескрип­ торного вспомогательного класса образуется из имени трансформируемого элемента и суффикса TagHelper . В рассматриваемом примере имя класса ButtonTagHelper сообщает инфраструктуре МVС о том, что это дескрипторный вспомогатель ный класс, который оперирует на элементах button. Область действия дескрипторного вспомо­ гательного класса может быть расширена или сужена с использованием атрибутов, описанных в разделе "Управление областью действия дескрипторного вспомогатель­ ного класса" далее в главе , но таково его стандартное поведение. Совет. Переопределяя метод ProcessAsync () вместо Process (), можно создавать асин­ хронные дескрипторные вспомогательные классы, но для большинства дескрипторных вспомогательных классов это не требуется, т.к. обычно они вносят в НТМL-элементы не­ большие и специализированные изменения. Стандартная реализация ProcessAsync () в любом случае вызывает метод Process () . Пример асинхронного дескрипторного вспомогательного класса приведен в главе 24. Получение данных контекста Дескрипторные вспомогательные классы получают информацию о трансформируе­ мом элементе через экземпляр класса TagHelperContext, который передается в качес­ тве аргумента методу Process () и определяет свойства, перечисленные в табл. 23.З. Таблица 23.3 . Свойства класса TagHelperContext Имя AllAttributes Items Uniqueid Описание Это свойство возвращает словарь только для чтения с атрибутами, примененными к элементу, подвергающемуся трансформации , который индексирован по имени и по числовому индексу Это свойство возвращает словарь, который используется для согласо­ вания дескрипторных вспомогательных классов, как описано в разделе "Согласование дескрипторных вспомогательных классов" далее в главе Это свойство возвращает уникальный идентификатор для трансформи­ руемого элемента Хотя получать доступ к деталям атрибутов элемента можно через словарь AllAttributes , более удобный подход предусматривает определ ение свойства, имя которого соответствует интересующему атрибуту, например: puЫic string BsButtonColor { get ; set ; } В случае применения дескрипторного вспомогательного класса инфраструктура MVC инспектирует определенные им свойства и устанавливает значения любых из них, имена которых совпадают с атрибутами, примененными к НТМL-элементу. Как часть этого процесса MVC будет пытаться преобразовать значение атрибута с целью соответствия типу свойства С# , так что свойства bool могут использоваться для по­ лучения значений булевских атрибутов (true и false), а свойства int - для получе­ ния з начений числовых атрибутов (наподобие 1 и 2 ).
Глава 23. Дескрипторные вспомогательные классы 717 Что произошло со вспомогательными методами HTML? В ранних версиях ASP.NET MVC для генерации элементов форм применялись вспомогатель­ ные методы HTML. Они представляли собой методы, обращение к которым осуществлялось посредством выражений Razor, начинающихся с @Html. Таким образом , элемент inpu t для свойства Popula t ion создавался бы следующим образом : @Html . TextBoxFor(m => m.P op ulati on ) Проблема выражений со вспомогательными методами HTML в том, что они не соответству­ ют структуре НТМL-элементов; это приводит к неудобным конструкциям вроде показанной ни же , которая добавляет стили Bootstrap к генерируемому элементу: @Html . TextBoxFor(m => m. Populat i on , new { @class = "form-control" }) Атрибуты должны выражаться в динамическом объекте и предваряться префиксом @, ког­ да они совпадают по написанию с зарезервированными словами С#, такими как c l ass. По мере усложнения требуемых НТМL-элементов выражения со вспомогательными мето­ дами HTML становятся все более неуклюжими. Дескрипторные вспомогательные классы избавляют от такой неуклюжести за счет использования атрибутов HTML : <input cl ass="form- contr ol" asp-for="Population" / > Результатом является более естественное соответствие природе HTML и возможность по­ строения представлений, которые легче в чтении и понимании. Инфраструктура MVC по­ прежнему поддер живает вспомогательные методы HTML (на самом деле дескрипторные вспомогательные классы внутренне применяют вспомогательные методы HTML). Это оз­ начает, что вы можете использовать их для обратной совместимости в представлениях, которые первоначально разрабатывались для версии MVC 5, однако новые представления должны задействовать в своих интересах более естественный подход, предлагаемый де­ скрипторными вспомо г ательными классами. Имя атрибута автоматически преобразуется из стандартного стиля HTML. bs - button -co l or , в стиль С#, т.е . BsButto nColor . Можно применять любой пре­ фикс атрибута кроме asp - (который использует Micгosoft) и d ata - (зарезервирован для специальных атрибутов, отправляемых клиенту) . В текущем пример е посредством атрибута BsBut t onColor получается цветовая схема для применения к элементу button в методе Process () : output .Att rib ut es.SetAttr ibu te ( " cl ass", $" btn Ьtn- ( BsButtonColor}" ) ; Свойства, для которых отсутствуют соответствующие атрибуты НТМL-элемента, не устанавливаются, т.е. вы должны удостоверяться , что не име ет е дело с null или стандартными значениями. В разделе "Управление областью действия дескриптор­ ного вспомогательного класса'" далее в главе объясняется, как изменять область дейс­ твия дескрипторного вспомогательного класса, чтобы он использовался только в от­ нош ении элементов, определяющих атрибуты , от которых вы зависите.
718 Часть 11 . Подробные сведения об инфраструктуре ASP. NEТ Core MVC Совет. Применение имен атрибутов HTML для свойств дескрипторного вспомогательного класса далеко не всегда приводит к получению читабельных или понятных классов . Вы можете разорвать связь между именем свойства и атрибутом , которое оно представляет, с использованием атрибута Htm l Att r ibuteName , позволяющего указывать представ­ ляемый свойством атрибут HTML. Генерирование вывода Метод Р r о се s s ( ) трансформирует элемент путем конфигурирования объ­ екта Tag He lpe rOu tp u t, который получается в качеств е аргу м ента. Объект TagHelpe r Ouput начинает сво е существовани е с описания НТМL -элемента в то м виде, как он появляется в представлении Razor, и модифицируе т его посредством свойств и метода, которые описаны в табл. 23.4 . Таблица 23.4 . Свойства и метод класса TagHelperOutput Имя TagName Attributes Content PreEl ement Pos tEleme nt PreCont ent Post Content TagMode SupressOuput () Описание Это свойство применяется для получения и установки имени де ­ скриптора в выходном элементе Это свойство возвращает словарь, содержащий атрибуты для выход­ ного элемента Это свойство возвращает объект TagHelp erCont ent, который ис­ пользуется для установки содержимого элемента Это свойство возвращает объект TagH e lperContext, который при­ меняется для вставки содержимого в представление перед вы ходным элементом . См . раздел "Вставка содер жимого перед и после элемен­ тов" далее в главе Это свойство возвращает объект TagHelperContext , который используется для вставки содержимого в представление после вы­ ходного элемента . См. раздел " Вставка содержимого перед и после элементов" далее в главе Это свойство возвращает объект TagHe l perCon text, который при­ меняется для вставки содержимого перед содер ж имым выходного элемента. См. раздел "Вставка содержимого перед и после элемен­ тов" далее в главе Это свойство возвращает объект TagHe l perContext, который ис­ пользуется для вставки содержимого после содер ж имого вы ходного элемента. См . раздел "Вставка содержимого перед и после элемен­ тов" далее в главе Это свойство указывает, как будет записываться выходной элемент, с применением значения из перечисления TagMode. См. раздел "Создание сокращающих элементов" далее в главе Вызов этого метода предотвращает включение элемента в представ­ ление . См. раздел " Подавление выходного элемента " далее в главе В классе ButtonTagHe l p e r с помощью словаря At t ribu tes к НТМL- эле ме нту до ­ бавляется атрибут clas s, который указывает стили Bootstrap для кнопки, включая значение свойств а BsBut t onCo l or, что означает возможность указания разных цве­ тов с использованием имен Bootstl'ap, таких как pri mary, info и danger.
Глава 23. Дескрипторные вспомогательные классы 719 Регистрация дескрипторных вспомогательных классов Дескрипторные вспомогательные классы можно применять только после того, как они будут зарегистрированы с использованием Rаzоr-выражения @addTagHelper. Набор представлений, к которым будет применяться дескрипторных вспомогатель­ ных классов, зависит от того, где используется выражение @addTagHelper. Для отдельного преставления это выражение присутствует в самом представле­ нии. Для подмножества представлений в приложении выражение @addTagHelper на­ ходится в файле_Viewimports. cshtml внутри папки, которая содержит представ­ ления, или внутри родительской папки. Таким образом, выражение @addTagHelper в файле /Views/Home / _ Viewimports . cshtml регистрирует дескрипторные вспо­ могательные классы для всех представлений, ассоциированных с контроллером Ноте . Нужно , чтобы дескрипторные вспомогательные классы были доступны во всех представлениях приложения, поэтому добавьте выражение @addTagHelper в файл Views/ _ Viewimports . cshtml, как показано в листинге 23.12 . Листинг 23. 12 . Регистрация дескрипторных вспомогательных классов в файле _ Viewimports. cshtml @using Cities . Models @addTagHelper Cities.Infrastructure.TagHelpers. * , Cities В первой части аргумента задаются имена дескрипторных вспомогательных клас­ сов, причем поддерживаются групповые символы, а во второй части указывается имя сборки, в которой они определены. В листинге 23.12 регистрируются все дескрипторные вспомогательные классы в пространстве имен Ci ties . Infrastructure . TagHelpers из сборки Ci ties. Использование дескрипторного вспомогательного класса Наконец, дескрипторный вспомогательный класс можно применить для транс­ формации элемента. Удалите атрибут class из элемента button в представлении Create . cshtml и замените его атрибутом, который ожидает класс ButtonTagHelper (листинг 23.13). Листинг 23.1 З. Использование дескрипторного вспомогательного класса в файле Create. cshtml @model City @{ Layout = "_Layout "; } <form method= "post" action= " /Home/Create"> <div class= " form - group"> <label for= " Name " >Name : </ label > <input class= " form-control" name="Name" /> </div> <div class="form- group " > <label for= " Country">Country:</label> <input class= "form - control" name="Country " /> </div> <div class= "form-group" > <label for= " Population " >Population : </label> <input class= " form- control " name="Population " /> </div>
720 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC <button type="suЬmit" bs-button-color="danger">Add</button> <а class="Ьtn Ьtn-primary" href=" /Home/Index">Cancel</a> </form> Запустив приложение и щелкнув на кнопке Create, вы заметите, что браузер за­ просит URL вида /Home/Crea te, после чего стиль и цвет кнопки Add изменятся (рис. 23.2). Population : l."--·-·--·--· ----"·-·-·-- -· ·- Рис. 23.2 . Применение дескрипторного вспомогательного класса для стилизации кнопки Модульное тестирование дескрипторного вспомогательного класса Модульное тестирование дескрипторного вспомогательного класса - относительно прос­ той процесс, который основан на снабжении метода Process () значащим содержимым, подлежащим обработке. Вот пример модульного теста для дескрипторного вспомогатель­ ного класса из листинга 23.11 : using System . Co ll ections . Generic ; using System . Linq ; using System . Threading . Tasks ; using Cities .Infrastructure . TagHelpers ; using Microsoft.AspNetCore.Razor.TagHelpers; usi ng Xunit; namespace Cities .T ests { puЫic class TagHelperTests [Fact] puЫic void TestTagHelper() 11 Организация var context = new TagHe l perContext( new TagHelperAttributeList(), new Dictionary<object , object>() , " myuniqueid " ) ; var output = new TagHelperOutput ( " button ", new TagHelperAttributeList() , (cache , en coder) = > Task .FromResult<TagHelperContent> (new DefaultTagHelpe r Content())) ; 11 Дейс т вие var tagHelper = new ButtonTagHelper BsButtonColor = " testValue " }; tagHelper . Process(context , output) ;
Глава 23 . Дескрипторные вспомогательные классы 721 11 Утверждение Assert.Equal($ " btn btn-{tagHelper.BsButtonColor}", output.Attributes[ " class "J . Value); Большая часть работы этого модульного теста связана с настройкой объектов TagHelperContext и TagHelperOutput, чтобы их можно было передать методу Process () дескрипторного вспомогательного класса и удостовериться в том, что НТМL­ элемент был трансформирован корректно. Объем работ, требуемых для подготовки де­ скрипторного вспомогательного класса к тестированию, вполне естественно зависит от сложности НТМL-разметки, которой он оперирует, и степени ее трансформации. Тем не ме­ нее, большинство дескрипторных вспомогательных классов будут относительно простыми и могут тестироваться в соответствии с приведенным ранее базовым шаблоном. Управление областью действия дескрипторного вспомогательного класса Дескрипторные вспомогательные К1Iассы прим еняются ко всем элементам задан­ ного типа, т.е. метод Process () класса ButtonTagHelper, созданного в предыдущем разделе, будет вызываться для каждого элемента button в каждом представлении внутри приложения. Это не всегда полезно. Чтобы более пристально взглянуть на проблему, добавьте в представление Create . cshtml еще один элемент button (лис­ тинг 23.14). Листинг 23.14. Добавление элемента Ьutton в файле Create. cshtml @model City @{ Layout = " _Layout "; } <form method="post " action="/Home/Create " > <div class= " form - group " > <label for="Name">Name : </label> <input class= " form - control" name="Name" /> </div> <div class= " form-group " > <label for = " Country " >Country : </label> <input class="form- control " name="Country" /> </div> <div class= " form-group " > <label for= " Population " >Population : </label> <input class= " form-cont r ol " name="Population" /> </div> <button type= " submit " bs -butt on-color= " danger">Add</button> <button type="reset" cJ..ass="btn btn-pri.mary">Reset</Ьutton> <а class= " Ьtn Ьtn-primary" href="/Home/Index">Cancel</a> </form> Новый элемент button уже имеет атрибут class и не требует трансформации, выполняемой классом ButtonTagHelper. Но если вы запустите приложени е и запро­ сите URL вида /Home/Create, то заметите, что возникла проблема, как иллюстриру­ ется на рис. 23.3.
722 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Population: Reset Рис. 23.3. Эффект от стандартной области действия дескрипторного вспомогательного класса Выяснить причину неудовлетворительного форматирования можно. просмотрев НТМL-разметку. которая отправлена браузеру , где обнаруживается пробл ема с атри­ бутом clas s: <button type="reset" class="btn btn-" >Reset</button> Инфраструктура MVC применила класс ButtonTagHelper к новому элементу button, но не установила значение свойства BsButtonColor из - за отсутствия соот­ ветствующего атрибута bs - button- color в НТМL-элементе. Это привело к тому , что дескрипторный вспомогательный класс заменил атрибут class атрибутом , в котором некорректно указаны стили Bootstrap, и в итоге получился неправильно сформатиро­ ванный элемент. Сужение области действия дескрипторного вспомогательного класса Для решения проблемы существуют два возможных подхода. П е рвый подход предусматривает модификацию класса ButtonTagHe l per, чтобы сделать его чувс­ твительным к разным элементам button, с которыми он может столкнуться. В рас­ сматриваемом пример е приложения это потр е бовало бы проверки , есть ли атрибут bs-button - color , и отказа от замены атрибута class , если он был определен. Проблема такого подхода в том , что по мере добавления в приложение пр едставле­ ний, содержащих элементы button, дескрипторный вспомогательный класс будет становиться все более и более сложным, причем вся привносимая сложность связа­ на с описанием условий , при которых класс ButtonTagHelper не должен выполнять свою трансформацию. Второй подход заключается в том, чтобы позволить определять ограничения на то, как используется дескрипторный вспомогательный класс, сужая область действия, в которой он будет применяться . Ограничения дескрипторного вспомогательного клас­ са указываются с использованием атрибута Html TargetElement (листинг 23.15). Листинг 23.15. Сужение области действия дескрипторного вспомогательного класса в файле ButtonTagHelper. cs using Microsoft . AspNe t Core . Razor .TagHe lpers ; namespace Cities . Infrastructure .T agHelpers { [HtmlTargetElement("button", Attributes = "bs-button-color", ParentTag = "form")] puЬlic class ButtonTagHelper : TagHelper puЬlic string BsButtonCol or { get ; set ;
Глава 23. Дескриnторные всnомогательные классы 723 puЫic override void Process(TagHelperContext context, TagHelperOutput output) { output .Attributes .SetAttribute("class", $"Ьtn Ьtn-{BsButtonColor)"); Атрибут Html TargetElement описывает элементы, к которым применяется де­ скрипторный вспомогательный класс. Первый аргумент указывает тип элементов и поддерживает дополнительные именованные свойства. перечисленные в табл. 23.5 . Таблица 23. 5 . Свойства атрибута Html TargetElement Имя Attributes ParentTag TagStructure Описание Это свойство используется для указания на то, что дескрипторный вспо­ могательный класс должен применяться только к элементам, которые имеют заданный набор атрибутов, предоставляемый в виде списка с раз­ делителями-запятыми. Элемент должен располагать всеми указанными атрибутами. Имя атрибута, которое заканчивается символом звездочки, будет трактоваться подобно префиксу, так что bs-button-* соответ­ ствует bs-button-color, bs-button-size и т.д. Это свойство используется для указания на то, что дескрипторный вспо­ могательный класс должен применяться только к элементам, которые содержатся внутри элемента заданного типа Это свойство используется для указания на то, что дескрипторный вспомогательный класс должен применяться только к элементам, чья структура дескриптора соответствует заданному значению из пере­ числения TagStructure, которое определено как Unspecified, NormalOrSelfClosing и WithoutEndTag В листинге 23.15 класс But tonTagHelper ограничен так, что он применяется только к элементам button, которые имеют атрибут bs-button-color, а их ро­ дительским элементом является form. Запустив приложение и запросив URL вида /Home/Create , вы заметите, что кнопка Reset (Сброс) больше не трансформируется, т.к. в ней отсутствует требуемый атрибут (рис. 23.4). 1 1 Population: l____ 1 _ ____ _, _____! Рис. 23.4 . Сужение области действия дескрипторного вспомогательного класса
724 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Расширение области действия дескрипторного вспомогательного класса Атрибут Ht mlTa r getElemen t может также использоватьс я для расширения об­ ласти действия дескрипторного вспомогательного класса , чтобы о н охв атывал боле е обширный диапазон элементов. Это полезно, когда одну и ту же трансформацию не ­ обходимо выполнять для нескольких типов эл ементов, что идет вразрез с исходны м условием сопоставления эл ементов на основ е имени де скрипторного вспомог ательно­ го класса (листинг 23.16). Листинг 23.16. Расширение области действия дескрипторного вспомогательного класса в файле ButtonTagHelper. cs u s ing Microsoft . AspNetCore . Razor . TagHe l per s; namespace Cities . Inf rast r ucture . TagHelpers { [HtmlTargetElement(Attributes = "bs-button-color", ParentTag = "form")] p uЬlic c l ass ButtonTagHelper : T agHelpe r { puЫic string BsButtonCo l or { get ; set ; } puЫic override void Process(TagHelperContext context , TagHe l per Output output) ( outp u t . Attri b utes . SetAttri bute( 11 cl ass", $"Ьtn Ьtn- (BsButtonColor} 11 ); В листинге 23.16 для атрибута Html TargetEl emen t тип эл ем ента не указан, а по­ тому дескрипторный вспомогательный класс будет применяться к любому элементу , который имеет атрибут bs - b u tto n- co lo r , независимо от типа э л е мента . В листин­ ге 23.17 элемент а внутри формы модифицирован так , что он использует такой же набор стилей Bootstrap , как и элементы b u t ton, поэто му он будет т рансформирован дескрипторным вспомогательным классом . Листинг 23.17. Модификация элемента а в файле Create. cshtml @model City @( Layout = 11 Layou t 11 ; } <f orm method= 11 post" acti on= 11 / Home/ Cr eate 11 > <div cla ss = 11 form - group 11 > <label for= 11 Name 11 >Name : </label> <iпput class= 11 f o rm- control 11 name= 11 Nam e" /> </div> <div cl ass= 11 form-group11> <label for=11Couпtry 11 >Couпtry : </label> <input clas s = 11 f orm- control 11 пame= 11 Country11 /> </ div> <div class= 11 form-group11> <labe l for= 11 Pop u lat ion 11 >Populat i on : </label> <input c l ass= 11 form- contr ol 11 name= 11 Popu l ation 11 /> </div> <buttoп type= 11 submit " bs-button-color= 11 da n ger 11 >Add</buttoп> <buttoп t ype= 11 rese t 11 class= 11 Ьtп Ьtn - pr i mary 11 >Reset</button> <а bs-button-color="primary" href="/Home/Index">Cancel</a> < /fo rm>
Глава 23. Дескриnторные вспомогательные классы 725 Возможность расшир ения области действия для дескрипторного вспомогательного кл асса означает, что вы не обязаны создавать дескрипторные вспомогательные клас­ сы, которые повторяют одну и ту же операцию в отношении разных типов элемен­ тов. Однако тр ебу етс я определенная о сторожность, поскольку довольно л е гко создать дес крипторный вспомогательный класс, который по мере будущего развития содер­ жимого представл ений в приложении начнет давать совпадения с элементами слиш­ ком широко . Более сбалансированный подход предусматрив ает применение атрибута Html TargetElement несколько раз с указанием полного набора эле ментов, которые будут трансформироваться, как 1юмбинации уз1ш определенных сопоставлений (лис­ тинг 23.18) . Листинг 23.18. Балансировка области действия дескрипторного вспомогательного класса в файле ButtonTagHelper. cs using Mi crosoft . AspNetCore . Razo r. Tag He lpers ; namespace Cities.Infrastructure . TagH elpers { [HtmlTargetEleme nt ("button", Attributes = 11 bs-button-color 11 , ParentTag = 11 form 11 )] [HtmlTargetElement("a", Attributes = 11 bs-button-color 11 , l?arentTag = "form")] puЫic cla s s ButtonTagHe lper : TagHelp er { puЫic st ring BsButtonColor { get; set; } puЫic ove r ride voi d Process(Tag He l perContext cont ext , TagHelperOutput output) { output . Attributes . SetAttribute( " class ", $ " Ьtn Ьtn -{ BsButtonColor }" ) ; } Та кая конфигур ация обеспечивает тот же самый результат в прилож е нии, но га ­ рантирует, что дескрипторный вспомогательный класс не будет вызывать проблем, если позже в процессе разработки атрибуты bs - b u tton- color начнут добавляться к другим типам элеме нтов по другой причине . Упорядочи вание выполнения дескрипторных вспомогательных классов В качестве общего правила рекомендуется использовать только один дескрипторный вспо­ могательный класс с любым заданным НТМL-элементом. Причина в том , что совсем не­ сложно попасть в ситуацию, когда один дескрипторный вспомогательный класс не счита­ ется с трансформацией , примененной другим дескрипторным вспомогательным классом, переопределяя значения атрибутов или содержимое. Если вы нуждаетесь в применении не­ скольких дескрипторны х вспомогательны х классов, тогда можете управлять последователь­ ностью их выполнения, устанавливая свойство Order , которое унаследовано из базового класса TagHelper . Управление последовательностью выполнения может помочь свести к минимуму конфликты между дескрипторными вспомогательными классами, хотя столкнуть­ ся с проблемами по-прежнему довольно легко .
726 Часть 11. Подробные сведения об инфраструктуре ASP. NEТ Саге MVC Усовершенство в анные возможности дескрипторных вспомогательных классо в В предыдущем разделе было продемонстрировано, каким образом создав ать и ис­ пользовать базовый дескрипторный вспомогательный класс, но это лишь малая толи­ ка доступных возможностей. В последующих разделах будут показаны бол ее сложные случаи применения дескрипторных вспомогательных классов и средств, которы е они пр едоставляют. Создание сокращающих элементов Дескрипторные вспомогательные Юiассы не ограничиваются трансформированием стандартных НТМL-элементов и могут также использоваться для замены специальных элементов ходовым содержимым. Данная возможность позволяет сделать пр едставле ­ ния более краткими и прояснить их замысел. Замените элементы b utton в представл е ­ нии Cr ea t e . c shtrnl специальными элементами, как показано в листинге 23.19. Листинг 23.19. Добавление специальных элементов в файле Create. cshtml @model City @{ Layout = "_Layout "; } <form method= "post " acti on=" / Home/Cr eate " > <div class="fo r m-group " > <l abel for="Name ">Name :</l abel> <input cl ass="form-control" name="Name" /> </di v> <div class="form-group"> <label for="Count r y">Country :</l abel> <input cl ass=" form- control" name="Country" /> </d iv> <div class=" form-gr oup"> <l abel f o r="Population " >Populati on: </l abel> <input class= " form- con trol" name= "Populati on " /> </di v> <formЬutton type="suЬmit" bg-color="danger" /> <formЬutton type="reset" /> <а bs -button- col or="pri mary" href=" /Home/Index">Cancel </a> </form> Элемент f o rrnЬut t on не входит в спецификацию HTML и не распознается браузе­ рами. Взамен мы собираемся применять эти элементы как сокращения для г ен ерации эл ементов b ut ton , которые требуются форме. Добавьте в папку Infrastructure/ TagHelper файл класса по имени F o rrnBut t onTagHelpe r. cs с определением из лис­ тинта 23 .20. Совет . Когда приходится иметь дело со специальными элементами, которые не являются частью спецификации HTML, должен применяться атрибут Htrnl Targ etE lernent и ука­ зываться имя элемента (листинг 23 .20) . Соглашение о применении дескрипторных вспо­ могательных классов к элементам на основе имени класса работает только для имен стандартных элементов.
Глава 23 . Дескрипторные вспомогательные классы 727 Листинг 23.20 . Содержимое файла ForrnВu ttonTagHelper. cs из папки Infrastructure/TagHelpers using Microsoft . AspNetCore . Razor .T agHelpers ; namespace Cities . Infrastructure .T agHelpers { [HtmlTargetElement( "formbutton " )J puЫic class FormButtonTagHelper : TagHelper puЫic string Туре { get; set ; } = "Submit "; puЫic string BgColor { get ; set; } = "pr imary "; puЫic override void Process(TagHelperContext context , TagHelperOutput output) { output . TagName = " button "; output . TagMode = TagMode . StartTagAndEndTag ; output . Attributes .SetAttribute( "class ", $ " Ьtn Ьtn - {BgColor} " ) ; output . Attributes .S etAttribute( "type ", Туре) ; output.Content . SetContent(Type == " submit " ? "Add ": " Reset " ) ; Метод Process () использует свойства объекта TagHelperOuput для генерации совершенно другого элемента. Свойство TagName применяется для указания эле­ мента button, свойство TagMode - для указания на то , что элемент записывается с испол ьзование м открывающего и закрывающего дескрипторов, метод Attributes . SetAt tribute () применяется для определения атрибута class со стилями Bootstrap, а свойство Con tent используется для установки содержимого элемента. Совет. Обратите внимание в листинге 23.20 на установку атрибута type выходного элемен­ та . Причина в том , что любой атрибут, для которого имеется свойство , определенное де­ с крипторным вспомогательным классом, в выходном элементе опускается . Обычно это хорошая идея, т.к . предотвращает появление атрибутов , применяемых для конфигуриро­ вания дескрипторных вспомогательных классов, в НТМL-разметке, отправляемой брау­ зеру. Тем не менее, в данном случае атрибут type используется для конфигурирования дескрипторного вспомогательного класса, поэтому необходимо, чтобы он присутствовал также в выходном элементе. Установка свойств а TagName важна из-за того, что выходной элемент по умолча­ нию записыва ется в таком же стил е, как специальный элеме нт. В листинге 23.19 при­ менялся самозакрывающийся дескриптор: <formbutton type= " submit " bg - color= " danger " /> Поскольку выходной элемент должен включать содержимое , нужно явно указать значение перечислени я TagMode . StartTagAndEndTag, чтобы использовались отде­ льные открывающий и закрывающий дескрипторы. Свойство Content возвращает экземпляр класса TagHelperContent , который прим е ня ется для установки содержимого элементов. В табл. 23.6 описаны самые важ­ ные методы класса TagHelperContent.
728 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Таблица 23.6. Полезные методы класса TagHelperContent Имя SetCon tent(te x t) Se tH t mlC on tent(html) Append(text) Ap pendHtm l (html ) Clear () Описание Этот метод устанавливает содержимое выходного элемента. Аргумент string кодируется так, чтобы безопасно вставляться в НТМL-элемент Этот метод устанавливает содержимое выходного элемента. Предполагается, что аргумент s tring закодирован как безопас­ ный . Должен использоваться с осторожностью Этот метод кодирует безопасным образом указанный аргумент s t ring и добавляет результат к содержимому выходного элемента Этот метод добавляет указанный аргумент s t ring к содержи­ мому выходного элемента, не выполняя кодирование. Должен применяться с осторожностью Этот метод удаляет содержимое выходного элемента В листинге 23.20 дескрипторный вспомогательный класс использует метод SetConten t () для установки соде ржимого выходного элемента на основе знач ения атрибута type , которое предоставляется через свойство Тур е . Запустив приложе­ ние и запросив URL вида /Home/Create . вы обнаружите, что специальны е эл еме нты fo rrnЬ utton были заменены стандартными НТМL-элементами. В итоге приведенные ниже элементы: <f ormb utton t ype= "submit" bg-color= "danger" /> <f ormbutton type= " reset " /> трансформировались следующим образом: <but ton class="Ьtn Ьtn-danger" type= " submi t">Add</button> <button class="Ьtn Ьtn-primary" t ype= "reset">Reset</button> Вставка содержимого перед и после элементов Класс TagHe lperOu tpu t предлагает четыре свойства, которые облегчают внед ­ рение нового содержимого в представление, так что оно окружает эл е ме нт или е го содержимое (табл. 23.7). Далее будет показано. как добавлять содержимое вокруг и внутрь целевого элемента. Таблица 23.7 . Свойства класса TagHelperOutput, предназначенные для дополнения содержимого и элементов Имя PreElement PostElement PreContent PostCon ten t Описание Это свойство применяется для вставки элементов в представ­ ление перед целевым элементом Это свойство используется для вставки элементов в представ­ ление после целевого элемента Это свойство применяется для вставки содержимого в целевой элемент перед существующим содержимым Это свойство используется для вставки содержимого в целевой элемент после существующего содержимого
Глава 23. Дескрипторные вспомогательные классы 729 Добавление содержимого вокруг выходного элемента Первые два свойства класса TagHelperOuput, PreElement и PostElement, применяются для вставки элементов в представление перед и после выходного эле ­ мента. Добавьте в папку Infras tructure/ TagHelpers файл класса по имени ContentWrapperTagHelper . cs и определите в нем дескрипторный вспомогатель­ ный класс, как показано в листинге 23.21 . Листинг 23.21 . Содержимое файла ContentWrapperTagHelper . cs из папки Infrastructure/TagHelpers using Microsoft . AspN etCore . Mvc . Rendering; using Microsoft.AspNe t Core . Razor.TagHelpers; namespace Ci ties .Infrastructure .TagHelpers { [HtmlTargetElement ( "div", Attributes = "title " )] puЫic class ContentWrapperTagHelper : TagHelper puЫic bool IncludeHeader { get; set; true; puЫic bool IncludeFooter { get; set; } = true ; puЫic string Title { get; set; ) puЫic override void Process(TagHelperContext context, TagHelperOutput output) { output .Attributes.SetAttribute("class", "panel -body"); TagBuilder title = new TagBuilder("hl" ) ; title.InnerHtml.Append(Title); TagBuilder container = new TagBuilder ( "div " ) ; container .Attributes[" class "] = "bg-info panel-body"; container .InnerHtml . AppendHtml(title); if (Inc lude Header) { output .PreE l ement .SetHtm lCon tent( container) ; if (Inc lude Footer) { output . PostElement.SetHtmlCon tent(container); Этот дескрипторный вспомогательный класс трансформирует элементы div. ко­ торые имеют атрибут title, с использованием свойств PreElement и PostElement для добавления верхнего и нижнего колонтитулов, 01<ружающих выходной элемент. При генерировании новых НТМL-элементов можно применять стандартное форма­ тирование строк С# для создания требуемого содержимого, но это неудобный и чре­ ватый ошибками процесс за исключением разве что самых простых элементов. Более надежный подход предусматривает использование класса TagBuilder, который оп­ ределен в пространстве имен Microsoft. AspNetCore. Mvc. Render ing и позволяет создавать элементы в лучше структурированной манере. Класс TagHelperContent определяет методы. принимающие объекты TagBuilder, которые облегчают созда­ ние НТМL-содержимого в дескрипторных вспомогательных классах.
730 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Соге MVC В дескрипторном вспомогательном классе ContentWrapperTagHelper с помощью класса TagBuilder создается элемент h l, содержащийся внугри элемента div, который был стилизован посредством классов Bootstrap . Имеются также необязательные атрибу­ ты i nclude- header и incl ude - foote r типа bool, применя емые для указания , куда внедрять содержимое; по умолчанию оно добавляется перед и после выходного элемента. В листинге 23.22 приведена обновленная компоновка, содержащая элемент, который бу­ дет трансформироваться дескрипторным вспомогательным классом . Листинг 23.22 . Включение дескрипторного вспомогательного класса в фaйлe _Layout.cshtml <!DOCTYPE html> <html > <h ead> <met a name= "viewport" cont ent="widt h=device- wi dth" /> <title>Cities</titl e > <l ink href="/liЬ/bootstrap/dist/css/bootstrap. css" rel = " stylesheet" /> </h ead> <body cl ass=" panel-body"> <div title=" Ci ties ">@RenderBody () </div> </body> </html> Запустив приложение, вы увидите, что дескрипторный вспомогательный класс применяется повсюду в приложении, добавляя верхний и нижний колонтитулы к каждой странице (рис. 23.5). Cities Nвmв.. · - . Populвtion Londo n UK 8, 539,000 Ne w York USA 8,406,000 San Jose USA 998,537 Paris France 2,244 ,000 - 1 Cities L.. _ ---·-·.. ------ Рис. 23.5 . Вставка НТМL-элементов с помощью дескрипторного вспомогательного класса
Глава 23. Дескрипторные вспомогательные классы 731 Вставка содержимого внутрь выходного элемента Свойства PreContent и PostContent используются для вставки содержимого внутрь выходного элемента, окружая первоначальное содержимое. Добавьте в папку Infrastructure/TagHelpers файл иласса по имени TaЬleCell TagHe lper. cs с оп­ ределением из листинга 23.23. Листинг 23.23. Содержимое файла TaЫeCell TagHelper. cs из папки Infrastructure/TagHelpers using Microsoft .AspN etCore .R azor .TagHelp ers ; namespace Cities . Infrast ructure.T agHe lper s { [H tmlTargetElement( "td", Attributes = "wrap ")J puЫic class TaЬleCellTagHelper : TagHelper { puЫic override void Process(TagHelperContext context, TagHe lperOutput output) output .PreCont ent .SetHtmlContent(" <b><i> "); output.PostContent .SetHtmlConte nt ( " </i></b> " ) ; Этот дескрипторный вспомогательный класс оперирует на элементах td с атрибу­ том wrap и вставляет элементы Ь и i вокруг содержимого выходного элемента. В лис­ тинге 23.24 демонстрируется добавление атрибута wrap к одной из ячеек таблицы внутри файла представления Index. cshtrnl. Листинг 23.24. Добавление атрибута HTML в файле Index. cshtml @model IEnumeraЬle<City> @{ Layout = " _Layout "; } <tаЫе class= " taЫe taЫe-condensed taЫe-bordered"> <thead class= " bg -primary" > <tr> <th>Name</th> <th>Country</th> <th class="text-right">Population</th> </tr> </thead> <tbody> @foreach (var city in Model) <tr> <td wrap>@city.Name</td> <td>@city.Country</td> <td class="text-right">@city.Population?.ToString("#,###")</td> </tr> </tbody> </tаЫе> <а href="/Home/Create" class="Ьtn Ьtn-primary">Create</a>
732 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC После запуска приложения вы заметите, что первая колонка ячеек в таблице, в которой перечислены объекты Ci ty, отображается полужирным курсивом. Заглянув в НТМL-разметку, отправленную браузеру, вы увидите, что содержимое, добавленно е через свойства PreContent и PostContent , находится с обеих сторон исходного со ­ держимого элемента: <tr> <td wrap><b><i>London</i></b></td> <td>UK</td> <td class="text- r i ght">B , 539 ,000</td> </tr> Совет. Обратите внимание, что атрибут wrap был оставлен на выходном элементе. Причина в том, что в дескрипторном вспомогательном классе не было определено свойство, соот­ ветствующее упомянутому атрибуту. Если вы хотите предотвратить попадание атрибутов в вывод, тогда определите для них свойства в дескрипторном вспомогательном классе , даже когда не собираетесь применять их значения. Получение данных контекста представления и использование внедрения зависимостей Одним из наиболее распространенных применений дескрипторных вспомогатель­ ных классов, вr<лючая встроенные классы такого рода, которые рассматриваются в главах 24 и 25, является трансформация элементов так , что они содержат детали теку­ щего запроса или текущей модели представления. Добавьте в папку Infrastructure/ TagHelpers файл класса по имени FormTagHe lper. cs , содержимое которого приве­ дено в листинге 23.25. Листинг 23.25. Содержимое файла FormTagHelper. cs из папки Infrastructure/TagHe lpers using Microsoft . AspNetCore . Mvc ; using Microsoft . AspNetCore . Mvc . Rend e ring ; using Microsoft . AspNetCore . Mvc . Routing ; using Microsoft .A spNetCore . Mvc . ViewFea tures ; using Microsoft . Asp NetCore.Razor .T agHelpers ; namespace Cities.Infrastructure . TagHe lpe rs { puЫic class FormTagHelper : TagHe l per { private IUrlHelperFactory urlHelperFact ory ; puЫic FormTagHelper(IUrlHelperFactory factory) urlHelperFac t ory = factory ; [ViewContext] [HtmlAttributeNotBound] puЬlic ViewContext ViewContextData puЫic string Controller { get ; set; puЫic string Action { get ; set ; } get; set; } puЬl i c override void Process(TagHelperContext context , TagHelperOu t p ut output) {
Глава 23. Дескрипторные вспомогательные классы 733 I UrlHe lpe r ur l Helper = u rlHelpe rFactory.Ge t Ur lHe l per(ViewContextData) ; ou tput.Att rib ut es.SetAt t ribut e("action", urlHelp e r.Action( Act ion ?? Vi e wC ontextData.Ro u teData . Val ues[" action "] .ToSt ring() , Cont r oller ?? ViewContextData.Rout eData .Values ["cont r oller"] .ToString ())) ; Как и можно было предположить по имени, класс FormT agHelper оперирует на элементах fo r m, уст анавливая их атрибуты action для указания, куда будут отправ­ ляться данные формы. Если элемент formимеет атрибуты cont r oller и a cti on, то их значения будут использоваться для генерации целевого URL; в противном случае будут применяться значения c o n t r o ller и acti o n из данных маршрутизации для текущего запроса. Чтобы получить данные контекста, понадобится добавить свойство по имени ViewContex t Data и декорировать его двумя атрибутами: [Vi.ewContext] [HtmlAttributeNotBound] puЫ ic ViewCo ntext ViewContextDa ta get; s e t ; } Атрибут ViewCon t ex t указыва ет на то, что значение этого свойства должно быть присвоено объекту ViewCo n te x t , когда создается новый экземпляр класса FormTagHelp e r, как объяснялось в главе 18. Класс ViewCon te xt предоставляет де­ тали о визуализируемом представлении, данных маршрутизации и текущем НТТР ­ запросе, как было описано в главе 21. Атрибут HtmlAt t rib uteNo t Bo und предотвращает присваивание инфраструк­ турой MVC значения данному свойству, если НТМL-элемент input имеет атрибут view - cont e xt . Это рекомендуемый прием, особенно если вы создаете дескрипторные вспомогательные классы для использования другими разработчиками. Совет. Существует встроенный дескрипторный вспомогательный класс для элемента form, который может применяться при нацеливании на методы действий и должен использо­ ваться в реальных проектах. Дескрипторный вспомогательный класс, рассматриваемый в настоящем разделе, предназначен просто для демонстрации, как можно применять дан­ ные контекста. За деталями об этом встроенном дескрипторном вспомогательном классе обращайтесь в главу 24. Дес1iрипторные вспомогательные классы могут объявлять в своих конструк­ торах зависимости от служб, которые распознаются с использованием средства внедрения зависимостей . В настоящем примере объявлена зависимость от службы IU rlHe lp e rF actory , которая позволяет создавать исходящие URL из данных мар­ шрутизации (и явля ется службой , лежащей в основе свойства Url, предоставляемо­ го классом Co ntroller . :который был описан в главе 16) . Внутри метода Pr o c e ss ( ) дескрипторный вспомогательный класс применяет метод IU rlHelperF act or y. GetU rlHe lper ( ) для получения объекта реализации IU r lHe l p e r, который конфи­ гурируется с помощью объекта Vi e wCo n t ext и затем используется при создании URL
734 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC для атрибута action выходного элемента. В листинге 23.26 демонстрируется подго­ товленное пр едставле ние , из которого удален атрибут action, так что его можно ус ­ танавливать посредством дескрипторного вспомогательного класса. Листинг 23.26. Удаление атрибута action из элемента form в файле Create. cshtml @model City @{ Layout= " Layout "; } <form method="post"> <div class= "form- group " > <label for= " Name " >Name : </label> <input c l ass= " form - control " name="Name " /> </div> <div class="form-group " > <label for="Country " >Country : </label> <input class= " form-control " n ame= " Country" /> </div> <div class= " form-group " > <label for= " Population " >Populat i on : </label> <input class= " form - control " name= " Population " /> </div> <formbutton type= " submit " bg-color= " danger " /> <formbutton type= "r eset " /> <а bs - button - color= " primary " href="/Home/Index">Cancel</a> </form> Запустив приложение , запросив URL вида /Home/Create и просмот р ев НТМL­ разметку. отправл енную браузеру, вы заметите, что элемент form имее т атрибут action, значение которого получено с примен ением данных контекста: <form method="post" action="/Home/Create" > Работа с моделью представления Дескрипторные вспомогательные классы могут оперировать на модели пред­ ставл ения, подгоняя выполняемые ими трансформации или создаваемый вы­ вод. Добавьт е в папку Infrastructure/TagHelpers файл класса по имени LabelAndinputTagHelper . cs с содержимым из листинга 23.27. Листинг 23. 27. Содержимое файла LabelAndinputTagHelper. cs из папки Infrastructure/TagHelpers using Microsoft . AspNetCore . Mvc.ViewFeatures ; using Microsoft . AspNetCore . Razor .T agHelpers ; namespace Cities . Infrastructure . TagHelpers { [HtmlTargetElement("label", Attributes = " helper-for " )] [HtmlTargetElement("input", Attributes = " helper - for " )] puЫic class LabelAndinputTagHelper : TagHelper { puЫic ModelExpression HelperFor { get; set; }
Глава 23 . Де с к рипторные вспомогательные классы 735 puЫic override void Process(TagHelperContext context , TagHelperOutput output) { if (output.TagName == " label " ) { output . TagMode = TagMode.StartTagAndE ndTag ; output . Content . Append(HelperFor . Name) ; output . Attributes.SetAttribute( " for ", HelperFor . Na me) ; else if (output.TagName == " input") { output . TagMode = TagMode . SelfClosi n g ; output . Attributes . SetAttribute( "n ame ", HelperFo r.N ame) ; output .Attribut e s. Set Attribute( " class ", " form- contr ol") ; if (Helpe r Fo r. Metadata . Mode l Type = = typeof(in t ?)) { output . Attributes . SetAttribute( " type ", " number " ) ; Де скрипторный вспомогат ельный класс LabelAndinputTagHelpe r трансформи­ рует элем енты l abel и input . которые имеют атрибут h elper - for . Важной частью этого дескрипторного вспомогательного класса является тип свойства He lper Fo r , ко­ торое используется для получения значения атрибута he l per - for : puЫic ModelExpression HelperFor { get ; set ; } Класс ModelExpression применяется, когда нужно оперировать с частью модели представления , что легче всего объяснить , продвинувшись дальше и показав , как де ­ скрипторный в с помогательный .кл ас с используется в пре дставлении (листинг 23.28) . Листинг 23.28. Применение дескрипторного вспомогательного класса, который оперирует на модели, в файле Crea te . cshtml @model Cities . Models . City @( Layout = " Layout "; } <form method= " post " > <div class= " form - group " > <laЬel helper-for="Name" /> <input helper-for="Name" /> </div> <div class= " form - group " > <label helper-for="Country" /> <input helper-for="Country" /> </div> <div class= " form - group " > <laЬel helper-for="Population"/> <input helper-for="Population" /> </div> <formbutton type =" subm i t " bg-color =" danger " /> <formbutton type= " reset " /> <а bs - button - color= " primary " href= " /Home/Index " >Cancel</a> </form>
736 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Значение атрибута helper-fo r - это свойство из IOiacca Model, которое обнару­ живается инфраструктурой MVC и предоставляется дескрипторному вспомогательно­ му классу как объект ModelExpres sio n. Класс Mo d el Ex pre ssi on здесь подробно не рассматривается, потому что любой анализ типов приводит к бесконечным спискам классов и свойств. Кроме того , в MVC имеется удобный набор встроенных дескрипторных вспомогательных классов. используемых моделью представления для трансформации элементов, как описано в главе 24, т.е. создавать собственные классы такого рода вам не придется. В примере дескрипторного вспомогательного класса задействованы две базовых возможности, которые полезно рассмотреть. Первая из них - получение имени свойс­ тва модели. так что его можно включить в выходной элемент: outp ut. Conte n t . Ap p end (HelperFor.Name ) ; output .Attri b utes . SetAttrib ute( "for ", He1perFor.Name) ; Свойство Na me возвращает имя свойства модели. Вторая возможность свя з ана с получением типа свойства модели, что позволяет изменить значение атрибута type элементов i n put : if (HelperFor.Metadata.Mode1Type == typeof (int?) ) { o utp ut. Att r ibutes.SetAttr i b ute("type", "number"); Запустив приложение, запросив URL вида / Ho me/ Cre a te и просмотрев НТМL­ разметку, которая была отправлена браузеру. вы обнаружите следующие элементы: <div class="form-group"> <l abel for= " Narne">Narne</l abel > <input name= "Narne " cl ass=" form-cont r ol" /> </di v> <div class="forrn-group"> <l abel for="Count ry">Country</l abel> <input name=" Country" cl ass=" forrn- cont r ol" /> </di v> <div cl ass= "form-group"> <label f or="Population ">Populat ion</l abel > <input n ame= "Population" cl a ss=" fo rm- con t rol" type=" n umber" / > </div > Атрибут type элемента i n put по имени Populatio n был установлен в n umber, подчеркивая тот факт, что свойство Ci ty. Popu lat ion в классе С# имеет тип int; это демонстрирует возможность отражения различных характеристик модели в НТМL­ разметке, генерируемой дескрипторным вспомогательным классом. В зависимости от применяемого браузера элемент i nput будет разрешать ввод только чисел . Согласование дескрипторных вспомогательных классов Свойство TagHe lperCo nt ext . Ite ms предоставляет словарь , который исполь зу­ ется для согласования дескрипторных вспомог ат ельных классов , оперирующих на элементах и их дочерних элементах. Чтобы ознакомиться с применением коллек­ ции Item s , добавьте в папку I nf r a s tructur e /TagHelpers файл класса по имени Coo r d inati n g TagHelper s . cs и определите в нем пару дескрипторных вспомога­ тельных классов, как показано в листинге 23.29.
Глава 23. Дескрипторные вспомогательные классы 737 Листинг 23.29. Содержимое файла CoordinatingTagHelpers. cs из папки Infrastructure/TagHelpers using Microsoft .AspNetCore .R azor . TagHelpers; narnespace Cities . Infrastructure.TagHelpers { [H trnlTarget Elernent ( "div", Attributes = "theme")] puЫic class ButtonGroupThemeTagHelper : TagHelper puЬlic string Theme { get; set ; ) puЫic override void Process(TagHelperContext context , TagHelperOutput output) { context . Items[ " theme "] = Theme; [HtmlTargetElement("button", ParentTag = "div")] [Htm1 'l'argetElement ("а", ParentTag = "div") ] puЫic class ButtonThemeTagHelper : TagHelper { puЫic override void Process(TagHelperContext context , TagHelperOutput output) { if (context . Items. ContainsKey ( "theme")) { output . Attr ibu tes .S etAttribute( "clas s ", $ "btn btn-{context.Items["theme"] }"); Первый дескрипторный вспомогательный класс, ButtonGroupThemeTagHelper, оперирует на элементах div, которые имеют атрибут theme. Согласование дескрип­ торных вспомогательных ш~ассов может трансформировать их собственные элемен­ ты, но в рассматриваемом примере оно просто добавляет значение атрибута theme в словарь Items, так что это значение становится доступным дескрипторным вспо­ могательным классам, которые оперируют на элементах, содержащихся внутри эле­ мента div . Второй дескрипторный вспомогательный :класс, ButtonThemeTagHelper, опери­ рует на элементах but ton и а, находящихся внутри элемента div. Он использует зна­ чение атрибута theme из словаря Items, чтобы установить стиль Bootstгap для своего выходного элемента. В листинге 23.30 приведен набор элементов, к которым будут применены эти дескрипторные вспомогательные классы. Листинг 23.30 . Применение согласованных дескрипторных вспомогательных классов в файле Create. cshtml @rnodel Cities . Models . City @{ Layout=" Layout";} <form method="post"> <div class= " form -gr oup " > <label helper-for= " Name" /> <input helper - for="Name" /> </div>
738 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC <div class="form-group"> <label helper- for=" Count r y" / > <input he l per-for=" Cou n try" /> </di v> <div class=" form- group " > <label he l per - for =" Population " /> <i n put helper-for= " Populat;ion " /> </di v> <div theme="primary"> <button type="suЬmit">Add</button> <button type="reset">Reset</button> <а href="/Home/Index">Cancel</a> </div> </ f orm> Запустив приложение и запросив URL вида / Home/Create , вы заметите, что груп­ па кнопок стилизована одинаково. Если вы замените значение атрибута theme эле­ м ента d i v друго й настройкой темы Bootstrap. такой как info , danger или primary, и затем перезагрузите страницу , то увидите , что изменени е отразилось в стилях кно­ пок (рис. 23.6) . •Е+Н' 1 ~...-..-J :"; t J.P"· Рис. 23.6. Согласование дескрипторных вспомогательных классов Подавление выходного элемента Дескрипторные вспомогательные классы могут использоваться для того, чтобы предотвратить включение элемента в НТМL-разметку, отправляемую браузеру, за счет вызова метода Su ppre ssOuput () на объекте TagHelpe r Output, который получается в качестве аргум е нта метода Pr o c es s () .В листинге 23.31 к раздел яемой компоновк е добавляется элемент, который отображает хорошо видимое сообщение , но только для запросов, направленных на заданное действи е. Листинг 23.31. Добавление хорошо видимого сообщения в фaйлe _Layout.cshtml <!DOCTY PE html> <html > <head> <meta name= " viewpor t " content= " width=device- wi dt h" /> <ti tle>Ci t i es</ti t le> <l i nk href= " /li Ь/bootstrap/dist/ c ss /bootstrap . css " rel= " stylesheet " /> </ h ead>
Глава 23. Дескрипторные вспомогательные классы 739 <body class="panel-body"> <div show-for-action="Index" class="panel-Ьody Ьg-danger"> <h2>Important Message</h2> </div> <div title="Cities">@RenderBody()</div> </body> </html> Атрибут show - for-action указывает имя действия, для которого необходимо отобразить сообщение. Такой подход к управлению включением содержимого нельзя считать приемлемым в реальном приложении, но его вполне достаточно для приме­ ра приложения с единственным контроллером и двумя действиями. Добавьте в папку Infrastructure/TagHelpers файл класса по имени SelectiveTagHelper. cs с со­ держимым из листинга 23.32 . Листинг 23.32. Содержимое файла SelectiveTagHelper. cs из папки Infrastructure/TagHelpers using System; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft . AspNetCore . Mvc.ViewFeatures; using Microsoft . AspNetCore . Razor.TagHelpers; namespace Cities . Infrastructure.TagHelpers { [HtmlTargetElement(Attributes = " show-for-action")J puЫic class SelectiveTagHelper : TagHelper { puЫic string ShowForAction { get; set; } [ViewContext] [HtmlAttributeNotBound] puЫic ViewContext ViewContext { get ; set; } puЫic override void Process(TagHelperContext context , TagHelperOutput output) { if (!ViewContext.RouteData . Values["action"] .ToString() .Equals{ShowForAction, StringComparison.OrdinalignoreCase)) output.SuppressOutput() ; С помощью объекта ViewContext дескрипторный вспомогательный класс получа­ ет значение action из данных маршрутизации и сравнивает его со значением атри­ бута show-for-action в НТМL-элементе. Если они не совпадают, тогда вызывается метод SuppressOutput (). Чтобы взглянуть на результат, запустите приложение и запросите URL вида /Home/Index и /Home/Create . Как показано на рис. 23.7 , сооб­ щение отображается только в случае нацеливания на действие Index .
740 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC х х . - -- ·· ··~ -~ -~ lo_.:all1~st_:~::::.'~~~~':cle~ -· -\ ~- ----' С' 1.J focall1os t:SS652/Home/Cr~at" Рис. 23. 7. Подавление элементов с применением дескрипторного вспомогательного класса Резюме В этой главе рассматривалось использование дескрипторнъrх вспомогательных классов, которые являются новым дополнением инфраструктуры ASP.NET Core MVC. Была объяснена роль, которую они играют в представлении Razor, а также продемонс­ трировано создание, регистрация и применение специальньrх дескрипторньrх вспо­ могательных классов . Вы узнали , как управлять областью действия дескрипторного вспомогательного класса, и ознакомились с разнообразными способами, которыми дескрипторные вспомогательные классы могут трансформировать НТМL-элементы . В следующей главе будут описаны дескрипторные вспомогательные классы , которые используются для работы с элементами НТМL-форм.
ГЛАВА 24 Использование дескрипторных вспомогательных классов для форм и нфраструктура MVC пр едлаг ает набор встро енных де скрипторных вспо мог а ­ тельных кла ссов, которые используются для выполнения часто требующих­ ся трансформаций в отношении НТМL- элементов . В этой глав е рассматриваются дескрипторны е в спом огательны е классы, оперирующие на НТМL- форм ах, которые предназначены для элементов form , i nput, label , select , option и textarea. В гл а ве 25 будут опи са ны други е встро енные дескрипторные вспо могательные клас­ сы, не относящиеся к формам . В табл. 24. l приведена сводка, позволяющая помес­ тить дескрипторны е в спомогательные классы для форм в конт екст. Таблица 24. 1. Помеще ние дескрипторных вспомогательных классов для форм в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Ответ Дескрипторные вспомогательные классы для форм применяются при трансформации элементов НТМL-форм, поэтому вам не при­ дется писать собственные классы такого рода, чтобы решать наибо­ лее распространенные задачи Дескрипторные вспомогательные классы для форм обеспечивают согласованную генерацию элементов внутри НТМL-форм , таких как label и inpu t . Большей частью дескрипторные вспомогательные классы гарантируют, что важные атрибуты наподобие .id, name и for устанавливаются напрямую с использованием классов моде­ лей представлений , но некоторые дескрипторные вспомогательные классы также могут генерировать содер жимое, заполняя, например, эле м енты select элементами option Встроенные дескрипторные вспомогательные классы ищут атрибу­ ты с префиксом asp-, такие как asp - for
742 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Окончание табл . 24. 1 Вопрос Ответ Существуют ли какие ­ то скрытые ловушки или ограничения? Единственное ограничение связано со способом, которым данные модели должны предоставляться дескрипторному вспомогатель­ ному классу, генерирующему элементы opti on внутри элементов s elect. Описание проблемы и специального дескрипторного вспомогательного класса, который ее решает, приведено в разделе "Работа с элементами select и option" далее в главе Существуют ли альтернативы? Создавать НТМL- формы в представлениях возможно вообще без при­ менения атрибутов дескрипторных вспомогательных классов. Можно также разработать собственные дескрипторные вспомогательные классы , используя приемы , которые были описаны в главе 23 Изменились ли они по сравнению с версией MVC 5? Дескрипторные вспомогательные классы для форм являются новым средством в ASP.NET Core MVC и предоставляют более элегантный подход, чем вспомогательные методы HTML , которые предлагали похо жую функциональность в предшествующи х версия х MVC. За дополнительными деталями обращайтесь во врезку " Что произошло со вспомогательными методами HTML?" в главе 23 В табл. 24.2 прив едена сводка для настоящей главы. Таблица 24.2 . Сводка по главе Задача Установка атрибута act i on в элементе form Препятствование подделке ме жсайтовых запросов Установка атрибутов id, name и val ue в элементе input Форматирование значения , отоб­ ражаемого элементом input Установка атрибута for и содер ж и­ мого элемента label Изменение содержимого элемента label, к которому был применен атрибут asp - for Установка атрибутов id и name в эле м енте select Генерация элементов opt i on Установка атрибутов id и n ame в эле м енте textarea Решение Используйте дескрипторный вспомо­ гательный класс для элемента form Примените атрибут ValidateAntiForger yToken к ме­ тоду действия и дополнительно уста­ новите атрибут asp- antiforgery в true для элемента form Листинг 24.1-24.5 24 .6, 24.7 Применяйте атрибут asp - for 24 .8 Примените атрибут asp - format 24 .9 -24.12 к элементу input или атрибут Displ ayFormat в классе модели Применяйте атрибут asp - for 24. 13 Примените атрибут Display к свойс- 24 .14 тву класса модели и с помощью свойства Name ука ж ите содержимое Пр и меняйте атрибут asp - for 24 .15 Применяйте атрибут asp - i tems 24 .16-24.21 Применяйте атрибут asp - for 24.22, 24.23
Глава 24 . Использование дескрипторных вспомогательных классов для форм 743 Подготовка проекта для примера Мы продолжим работать с проектом Ci ties, созданным в предыдущей главе. Сначала потребуется провести небольшую подготовку, отключив специальные де­ скрипторные вспомогательные классы, возвратив представления к использованию стандартной НТМL- разметки и добавив пакет NuGet, который будет задействован позж е в главе. Изменение регистрации дескрипторных вспомогательных классов Для целей настоящей главы необходимо включить встроенные дескрипторные вспомогательные классы. поступающие с инфраструктурой MVC, и отключить специ­ альные дескрипторные вспомогательные классы, которые были созданы в главе 23. В листинге 24. l показаны изменения. которые понадобится внести в файл импорти­ рования представлений, где выражение @addTagHelper для вспомогательных клас­ сов из сборки Ci ties заменено выражением. настраивающим вместо них дескрип­ торные вспомогательные классы MVC. Листинг 24. 1. Изменение дескрипторных вспомогательных классов в файле _ Viewimpo rts . cshtml @using Cities . Models @addTagHelper *, Microsoft.AspNetCo re.Mvc.TagHelpers Встроенные дескрипторные вспомогательные классы определены в сборке по имени Microsoft . AspNetCo re . Mvc. Ta gHelpe rs, которая добавляется в проект как зависимость пакета Micr osoft . AspNetCore. Mvc , перечисленного в файле proj ect . j son. Переустановка представлений и компоновки В листинге 24.2 приведено содержимое представления Index . cshtml , из которого удалены атрибуты . применяемые специальными дескрипторными вспомогательными классами. Листинг 24 .2 . Содержимое файла Index . cshtml @model IEnumeraЬle<Cit y> @{ Layout = " Layout "; } <tаЫе class= "t aЫe taЬle -c onden s ed taЬle - bordered"> <thead cl ass= "bg-primary"> <tr> <th>Nam e< / th> <th>Country</th> <th class= "text-right " >Popu l ation</th> </tr> </thead> <tbody> @foreach (var city in Model) <tr> <td>@c ity . Name</td> <td>@city . Country</td>
744 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC <td class="text-right">@city.Population?.ToString("#,###")</td> </tr> </tbody> </tаЫе> <а href= " /Home/Create " c l ass= "Ьt n Ьtn-primary">Create</a> В листинге 24.3 показаны соответствующие изменения файла Create . cshtml, который возвращен 1< использованию стандартных НТМL-элементов без атрибутов, применяемых в главе 23. Листинг 24.З. Содержимое файла Create. cshtml @model City @{ Layout = " Layout "; } <form method="post" act i on= " /Home/Create " > <div class= " form-group " > < l abel for= " Name " >Name:</labe l > <input c l ass= " form -control " name="Name" /> </div> <div class= " form - gro up" > < l abel for="Country">Country:</label> <input class= " form -con trol " name="Country" /> </di v> <div class= " form-group " > <labe l for="Population">Population:</label> <input class= " form - control" name="Population" /> </div> <button type="submit " c l ass= "Ьtn Ьtn-primary">Add</button> <а class= " Ьtn Ьtn-primary " href= " /Home/Index">Cancel</a> </form> Последнее изменение касается разделяемой компоновки (листинг 24.4) . Листинг 24.4 . Содержимое фaйлa _Layout.cshtml <!DOCTYPE html> <html> <head> <meta name= " viewport " content="width=device-width" /> <title>Cities</title> <link href="/liЬ/bootstrap/dist/css/bootstrap . css" rel="stylesheet" /> </head> <body class="panel-body"> <div>@RenderBody()</div> </body> </htm l > Запустив приложение, вы увидите список городов; щелкнув на кнопке Create (Создать), можно заполнить форму и отправить новые данные серверу (рис. 24.1).
Глава 24 . Использование дескрипторных вспомогательных классов для форм 745 D Cit", х D Citios Х ~- [p_1~~-~~49l7~--- -----==,-: - - :r1 ~-1--~--~~~ocalhost~~7.!!Ho1,';;/Create ___ ii]_~ London UK 8,539,000 ,, 1 Namв: New York USA 8,406,ООО !I Country : 1 San J ose USA 998,537 111 Nam~·" ". · ' ' ·еоuПtТУ .·. :'' .· -'. ·J \J pulation ,, ., ' Par1s Francв 2,244.000 1 Populatio n: :• 1 L______________________J, 111!'*!1!_________ -- - Рис. 24.1. Выполнение примера прило жения Работа с элементами f orm. Класс Fo rmTagHelper - эт о встроенный дескрипторный вспомогательный класс для элементов form, который используется для управления конфигураци ей НТМL­ форм, так что их можно нацеливать на правильный метод действия, основываясь на конфигурации маршрутизации приложения . Дескр ипто рный вспомогательный класс FormTagHelper поддер живает атрибуты, описанные в табл. 24.3 . Таблица 24.3 . Атрибуты встроенного дескрипторного вспомогательного класса для элементов form Имя asp- control l er asp- action Описание Этот атрибут применяется, чтобы указать системе маршрутизации значение controller для URL в атрибуте action. Если он опу­ щен, тогда будет использоваться контроллер, визуализирующий представление Этот атрибут применяется, чтобы указать системе маршрутизации ме­ тод действия для URL в атрибуте action. Если он опущен, тогда будет использоваться действие, визуализирующее представление asp- r oute- * Атрибуты с именами, начинающимися с asp- route-, применяются для указания дополнительных значений URL в атрибуте action , так что атрибут asp- route-id используется для предоставления систе­ ме маршрутизации значения сегмента id asp- route Этот атрибут применяется для указания имени маршрута, который бу­ дет использоваться при генерации URL в атрибуте action asp- area Этот атрибут применяется для указания имени области, которая будет использоваться при генерации URL в атрибуте action asp- antiforgery Этот атрибут управляет добавлением информации, противодейству­ ющей поддел ке , как объясняется в разделе " Использование средства противодействия подделке" далее в главе
746 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Установка цели формы Основным назначение класса Fo rmTa gH elpe r является установка атрибута action элемента form с применением конфигурации маршрутизации приложения , гарантируя тем самым , что данные формы всегда посылаются корректному URL, даже когда схема маршрутизации изменяется. В листинге 24.5 используются атри­ буты asp - a c tion и as p-controller для нацеливания на метод действия Create () контроллера Home . На заметку! Дескрипторный вспомогательный класс не устанавливает атрибут me t hod, и если он отсутствует в элементе form , тогда браузер будет применять запрос GET для отправки данных формы клиенту. Как объяснялось в главе 17, это может вызвать про ­ блемы, если данные формы используются для модификации данных в приложении. Рекомендуется устанавливать атрибут method, даже если нужны запросы GET , чтобы стал очевидным тот факт, что вы не забыли выбрать тип метода. Листинг 24.5 . Установка цели формы в файле Create. cshtml @model City @{ Layout = " Layout"; } <formmethod="post" asp-controller="Home" asp-action="Create"> <di v class=" form-group"> <label for =" Name " >Name : </label> <input class=" form- control" name="Name" /> </div> <div cl ass=" form-group"> <l abel for= " Country">Country: </l abel> <input cl ass=" form-control" name=" Count ry" /> </ div> <div class= " form-group"> <label for= " Population" >Po pu lati on : </label> <input cl ass= " form - control " name ="Populati on " /> </div> <button type= " submi t " class="Ьtn Ьtn-primary">Add</button> <а cl as s ="btn btn-primary " href=" / Home/Index " >Cancel</a> </form> Запустив приложение , запросив URL вида /Home/C r eate и просмотр е в НТМL ­ разметку, которая была отправлена клиенту, вы заметите , что дескрипторный вспо­ могательный класс добавил к элементу f orm атрибут a c ti on и установил его значе­ ние с применением системы маршрутизации: <f orm me t hod= "pos t " action="/Home/Create"> Использование средства противодействия подделке Подделка межсайтовых запросов (cross-site requ est forgery - CSRF) - это спо соб злоумышленной эксплуатации веб-приложения, направленный на то, чтобы задейс­ твовать в своих интересах метод аутентификации пользовательских запросов . В боль ­ шинстве веб-приложений, включая создаваемые с использованием ASP.NET Core, идентификация запросов, относящихся к специфическому сеансу, с которым обычно ассоциирован пользователь , осуществляется с применением сооkiе-наборов.
Глава 24 . Исnользование дескриnторных всnомогательны х классов для форм 747 Атака CSRF (также называемая мистификацией сеанса) опирается на ситуацию, когда пользователь посещает злоумышленный веб-сайт после работы со своим веб­ прило же нием, явно не завершив свои сеансы щелчком на кнопке выхода из прило­ ж ения. Прилож ение по - прежнему с читает пользоват еля активным, а сооkiе-набор, ко ­ торый браузер сохранил, пока еще не истек . Злоумышленный веб-сайт содержит код JavaScript, отправляющий приложению запрос формы, который выполняет операцию бе з согл асия поль зователя; при этом природа операции будет зависеть от приложе­ ния, подвергаемого атаке . Поскольку код JavaScript выполняется браузером пользо ­ вателя, запрос к приложению включает сооkiе-набор сеанса и приложение выполняет операцию безо всякого уведомления пользователя или согласия с его стороны. Если элемент form не содержит атрибут ac tion (из-за того, что он генерирует­ ся системой маршрутизации с помощью атрибутов asp - con troller и a s p- acton), то класс FormTag Helpe r автоматически включает средство противостояния ата ­ кам CSRF. Указанное средство добавляет к форме скрытый элемент inpu t с марк е ­ ром безопасности внутри НТМL-разметки, которая отправляется клиенту наряду с сооki е- набором. Приложение будет обрабатывать запрос, только если он содержит и сооkiе -набор , и скрытое значение формы, к которому злоумышленный веб-сайт не имеет доступа. Каждый запрос к форме генерирует новый уникальный набор марке­ ров б е зопасности. Запустите приложение , запросите URL вида /Home/Crea t e и просмотрите НТМL­ разметку, отправленную браузеру: в ней обнаружится СI<рытый элемент i n put вроде показ анного ниже: <input name= " RequestVer i ficationToken " type= " hidden " va lue =" CfDJ8KuVkH8h FlRApe FBxTrhCFTKZe0B9BKw n W DJ q L R U Dk~Pr E waeCJm i BbG k wW1 Z I816c TrM5XQkJBeqNI5IL8 FhuOR v jZuYIL-G Zv nWZ620ThsZYT 02 HNX Lu5LW DNWDdVoS505h Zt z aoH LeY5 1N to " /> - Воспользовавшись инструментами браузера, доступными по нажатию клавиши < F12 >. можно такж е увидеть соответствующий сооkiе-набор , который добавляется к отв ету. Добавление маркеров безопасности к НТМL-ответам - лишь часть про­ цесса; они должны также пров еряться контроллером, как продемонстрировано в листинг е 24.6 . Листинг 24.6. Проверка маркеров противодействия подделке в файле HomeController. cs using Microsoft . AspNe t Co re . Mvc ; using Cities . Models ; namespace Ci ties . Contro ll ers { puЫic class HomeController : Co n t r oll er { privat e I Reposito ry rep os i tor y; puЫic HomeController( IRepos itory r epo) { repository = repo ; puЫ i c ViewRes u lt I ndex() => View(rep os i tory . Cities) ; puЬlic ViewResult Cr eate() => Vi ew() ; [HttpPost ] [ValidateAntiForgeryToken]
748 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic IActionRe s ul t Create(City c ity) r e pository . AddCit y( city) ; retur n RedirectToActi on( "Index " ) ; Атрибут Va lidate AntoFo r ge r yToken удостоверяется, что запрос содержит до­ пустимые маркеры противодей ствия атакам CSRF, и будет генериров ать исключ ение , если они отсутствуют или не имеют ожидаемые значения. Класс FormTagHelper предлагает атрибут asp - antiforgery для переопредел е ­ ния стандартного пов едения противодействия ат акам CSRF. При установке этого ат­ рибута в true маркеры безопасности будут включаться в запросы , даж е если элемент form имеет атрибут action. Когда значением атрибута a s p-antiforgery явля ется fa l se , мар1< е ры безопасности будут отключены . В листинге 24.7 средство противо­ действия атакам CSRF включается явно, хотя маркеры безопасности были бы добав­ лены в любом случае, потому что для элемента form не определен атрибут act i on . Листи нг 24.7 . Включение средства противостояния атакам CSRF в файле Create. cshtml @rnodel City @{ Layout = " Layout "; } <formmethod= "post" asp-controller="Home" asp-action="Create" asp-antiforgery="true"> <di v class="forrn- group"> <label for="Narne">Narne :</label> <input class=" forrn- cont rol " narn e="Narne " /> </div> <div class="forrn- group"> <l abel for= "Country">Count ry :</label > <input class= " forrn - control " narne =" Country " /> </div> <div class= " forrn- group " > <label for= " Population " >Populati on: </l abel > <i nput class=" forrn- control " narne=" Popul at ion" /> </div> <button type= " subrnit " c l ass= " btn btn - pri rnary " >Ad d </button> <а class= " btn btn - prirnary " href=" / Horne/Index " >Cancel</a> </forrn> Совет. Тестирование средства противодействия атакам CSRF связано с некоторыми ух ищ­ рениями. Это делается путем запрашивания URL, который содержит форму ( / Home / Create в рассматриваемом примере), и последующего применения инструментов <F12> браузера для нахождения и удаления из формы скрытого элемента input (либо измене­ ния его значения) . После заполнения формы и ее отправки приложению браузер не будет иметь одну часть требующихся данных, в результат чего запрос должен завершиться не­ удачей с отобра жением страницы ошибки.
Глава 24. Использование дескрипторных вспомогательных классов для форм 749 Работа с элементами input Элемент input является основой НТМL-форм и предоставляет главные средства, с помощью которых пользователь может снабжать приложение неструктурированными данными. Класс I npu t TagHel per используется для трансформирования элементов i n p u t , чтобы они отражали тип данных и формат свойства модели представления , применяемого для сбора информации, с использованием атрибутов из табл. 24.4 . Таблица 24.4 . Атрибуты встроенного дескрипторного вспомогательного класса Имя asp - for asp-format для элементов input Описание Этот атрибут применяется для указания свойства модели представления, которое олицетворяет элемент i n put Этот атрибут используется для указания формата, применяемого при отоб­ ражении значения свойства модели представления, которое олицетворяет элемент inpu t Конфигурирование элементов i.nput Атрибут asp- fo r устанавливается в имя свойства модели представления, 1юторое затем используется для установки атрибутов n arn e , id, type и value элемента input . В листинге 24.8 демонстрируется применение атрибута asp -for к элементам i nput внутри представления Cre a te . cshtrnl . Листинг 24.8 . Конфигурирование элементов input в файле Create. cshtml @model City @{ Layout = " Layou t"; } <form method= "post " asp- contr ol ler= " Horne " asp- action= " Create" asp-anti forgery=" t r ue"> <div class= " form- group " > <label for= " Name " >Name : </lab el> <input class="form-control" asp-for="Name" /> </div> <di v class== " form-group"> <label for= " Count r y">Count ry: </label> <input class="form-control" asp-for="Country" /> </div> <div class= " form- gr oup"> <label for= " Populati on">Popul ation: </l abel > <input class="form-control" asp-for="Population" /> </div> <button type= " subm i t " class= " Ьtn Ьtn -pri mar y" >Add</button> <а class="Ьtn Ьtn-primary" href=" /Home/Index">Cancel </a> </form> Запустив приложение и запросив URL вида / Ho me/Creat e, вы обнаружите, что дескрипторный вспомогательный класс использовал свойство, указанное в атрибуте asp- for , для подстройки каждого элемента inpu t, как показано в следующем фраг­ мент е (марк е р б е зопасности, защищающий от атак CSRF, опущен) :
750 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC <fo r m method= " post " action=" /Home/C r eate " > <d i v class= " form-group " > <label for= " Name " >Name : </label> <input c1.ass="form-contro1." type="text" id="Name" name="Name" va1.ue="" /> </div> <di v class= "form- group"> <label for= " Country " >Co u ntry : </label> <input class="form-control" type="text" id="Country" name="Country" value= "" /> </div> <div class= " form- group "> <label for= " Population " >Popu l ation : </label> <input class="form-control" type="numЬer" id="Population" name="Population" value="" /> </div> <bu t ton type=" submit " c l ass= " Ьtn Ьtn- primary " >Add</bu tt on> <а class= " Ьtn Ьtn - primary " href= " /Home/Index " >Cancel</a> </form> Атрибут type элемента input сообщает браузеру о то м , к ак отобр аж ать эл е м ен т в форме. Вы можете просмотреть результат этого процесса в элемент е inpu t для свойства Populat i on, атрибут type которого был установлен в numЬer . Дел о в том , что типом С # свойства Population является int ?; таким образом , дескриптор ный вспомогат ельный класс применя ет атрибут type, чтобы указать брауз еру о приняти и только числ овых знач ений. На заметку! Способ интерпретации атрибута type возлагается целиком на браузер. Не все браузеры реагируют на все значения атрибута type, которые определены в специфи ка­ ции HTML5, и даже когда они реагируют, существуют отличия в том, к а к им образом они это делают. Атрибут t уре может быть удобной подсказкой о разновидности данных , кото­ рые ожидаются внутри формы, но чтобы гарантировать предоставление пользователями пригодных данны х, обязательно дол ж но использоваться средство проверки достовернос­ ти модели, ка к объясняется в главе 27 . В табл . 24.5 описан способ примен ения различных типов св ойств С# для установ­ ки атрибута type элем е нтов input . Таблица 24.5. Типы свойств С# и атрибуты type эле м ентов input, которые они генерируют Тип С# Атрибут type элемента input byte,sbyte, int,uint,shor t, numЬer ushort , long, ulong floa t , douЬle , decimal text с дополнительными атрибутами для проверки до­ стоверности модели, как будет показано далее в главе bool checkЬox str ing text Dat eTime datet ime
Глава 24 . Использование дескрипторны х вспомогательных классов для форм 751 типы float, douЫe и decirnal выпускают элементы input с ат рибутом type , установленны м в text, т.к . н е все браузеры разрешают использов ать полный диапа­ зон символов для выражения допустимых значений указанных типов. Чтобы оказать помощь пользователю , дескрипторный вспомогательный класс добавляет к эл ементу input атрибуты , которые прим е няются со средством проверки достоверности м одели (глава 27) . Стандартны е отображе ния из табл. 24.5 можно пер е определ ить, определив атри­ бут type эле ме нта i nput . Дескрипторный вспомогат ельный класс не будет переопр е ­ делять указанно е в type значение, что позволит задействовать разнообразны е до­ ступные типы элементов input , таки е как pass wo r d или h idden , либо новые типы, появившиеся в HTML5, наподобие numЬer. Недостаток этого подхода в том, что приходится помнить о н еобходимости уста­ новки атрибута type во всех представлениях , где элем енты input генерируются для заданного сво йства модели . Если нужно переопределить стандартное отображение во множестве пр едставл ений , тогда можно применить атрибут UI Hint к свойству в клас­ се модели С# и уиазать в иачестве его аргумента одно из значений, перечисленных в табл. 24.6 . Совет. Дескрипторный вспомогательный класс будет устанавливать атрибут type элементов input в text, когда тип свойства модели не является одним из описанных в табл. 24 .5 и не де корирован атрибутом UI Hin t. Таблица 24.б. Аргументы атрибута UIHint и генерируемые ими значения атрибута type элементов input Значение Hiddeninput Password Text PhoneNwnЬer Url ErnailAddress Time Date DateTime- local Атрибут type элемента input hidden passwo r d text tel url ernail time (это значение используется для отобра жения компонента времени объекта DateTi me) date (это значение применяется для отображения компонента даты объекта DateTirne) datetirne- local (это значение используется для отображения объекта DateTime без информации о временном поясе) Форматирование значений данных Когда метод действия снабжает представление объ ектом модели представления , десирипторный в с помогат ельный ил асс применяет значение свойства , указанное в ат ­ рибуте asp - for , для установки атрибута va l ue элемента i nput . Атрибут asp- format используется для указания, Iiаким образом форматировать это значение данных.
752 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Чтобы продемонстрировать сказанное, в контроллер Ноте добавлен новый метод действия (листинг 24.9), который выбирает из хранилища первый объект Ci ty и при­ меняет его в качестве модели представления для Create. cshtml. Листинг 24.9 . Добавление метода действия в файле HomeController. cs using Microsoft.AspNetCore . Mvc; using Cities . Models; using System.Linq; namespace Cities . Controllers puЫic class HomeController : Controller { private IRepository repository; puЫic HomeController(IRepository repo) { repository = repo; puЬlic ViewResult Index() => View(repository . Cities); puЫic ViewResu1 t Edit () => View ( "Create" , reposi tory. Cities. First () ) ; puЫic ViewResult Create() => View(); [HttpPost] [ValidateAn tiForgeryToken] puЫic IActionResult Create(City city) repository . AddCity(city); return RedirectToAction("Index"); Запустив приложение, запросив URL вида /Horne/Edi t и просмотрев НТМL­ разметку, которая была отправлена браузеру, вы заметите, что атрибуты value за­ полнились с использованием объекта модели представления, например: <input class= " form-control" type="number" id= "P opulation" name="Population" va1ue="8539000" /> Атрибут asp - format принимает значение, которое будет передано стандартной системе форматирования строк С# (листинг 24.10). Листинг 24.1 О. Форматирование значения данных в файле create. cshtm1 @model City @{ Layout = " Layout"; } <form method="post" asp-controller="Home" asp - action="Create" asp-antiforgery="true"> <div class="form-g roup"> <label for="Name " >Name : </label> <input class="form - control " asp-for="Name" /> </div> <div class="form- group " > <label for="Country " >Country : </label> <input class="form - control " asp-for="Country" /> </div>
Глава 24. Использование дескрипторны х вспомогательных классов для форм 753 <div class= " form - group"> <label for="Population " >Population : </label> <input class="form-control" asp-fo="Population" asp-format="{O:#,###}" /> </div> <button type= " submit " class= " btn btn - primary " >Add</button> <а class= " btn btn - primary" href="/Home/Index">Cancel</a> </form> Значение атрибута применяется дословно, т.е. вы обязаны включить фигурные скобки и ссылку О : , а также требуемый формат. В результате запуска приложения и запроса URL вида /Home/Edit , вы заметите, что значение Population было сфор­ матировано следующим образом: <input class="form- control" type="number" id= " Population " name= " Population " value="8 ,539,000" /> Данное средство должно использоваться с осторожностью, т.к. вам нужно гаран­ тировать. что остальная часть приложения сконфигурирована для поддержки подоб­ ного формата. В рассматриваемом случае возникает проблема из-за форматирования значения Population. Дескрипторный вспомогательный 1ишсс установил атрибут type элемента input для свойства Population в nwnЬer с применением стандарт­ ных отображений, описанных в табл. 24.5, но указанная строка формата сгенериро­ вала атрибут value, который содержит нецифровые символы. В результат е браузеры, поддерживающие тип элемента nwnЬer (вспомните, что не все браузеры это делают). могут вообще не отобразить какое-либо значение в элементе. В приложении необходимо также обеспечить возможность разбора значений в ис­ пользуемом формате. Пример приложения ожидает получать значение Population, которое может быть разобрано в int, так что значения, содержащие нецифровые символы, приведут к ошибкам проверки достоверности (глава 27). Применение форматирования через класс модели Если вы хотите всегда использовать для свойства модели одно и то же форма­ тирование, тогда можете декорировать класс С# атрибутом DisplayFormat, кото­ рый определен в пространстве имен System . ComponentModel .DataAnnotations. Для форматирования значения данных атрибут DisplayFormat требует двух ар­ гументов: аргумент DataFormatString указывает строку формата, а аргумент Appl yForma t InEdi tMode - что форматирование должно применяться при редак­ тировании значений. В листинге 24.11 атрибут Population декорирован атрибутом DisplayFormat с использованием формата, который может быть обработан прило­ жением и браузером как числовой. Листинг 24.11 . Применение атрибута форматирования к классу модели в файле Ci ty. cs using System.Componentмodel.DataAnnotations; namespace Cities.Models { puЫic class City { puЫic string Name { get; set; } puЫic string Country { get ; set; [DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatinEditмode = true)] puЫic int? Population { get; set; }
754 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Атрибут asp - format имеет преимущество перед атрибуто м Disp l ayFormat, поэ­ тому он удале н из пр едставл ения (листинг 24.12). Листинг 24.12. Удаление атрибута форматирования в файле Crea te. csh trnl @mo d el City @{ Layout = " Layout"; } <for m method= " post " asp- controll er= " Home " asp- action= " Create " asp- anti forgery= " true"> <div class=" form-group"> <l abel for= " Name " >Name : </label> <input class= " form- control " asp- for= " Name " /> </div > <div class=" form- group"> <label for= "C ountry">Country : </l abel> <i nput class = " form- control " asp- for= " Country" /> </div> <div class= " form- group"> <label for= " Popu l ation " >Popul ation : </ l abel> <input class="form-control" asp-for="Population" /> </div> <butt o n type= " s u bmit " class= " Ьtn Ьtn - primary " >Add < /button> <а cl a ss = " Ьtn Ьtn - primary " href= " /Home/Index " >Cancel</a> < /form> Запустив приложение и запросив URL вида / Home/Edi t , вы обнаружите, что з на­ чение P o pulation было сформатировано с двумя знаками после десяти ч ной точки : <input class= " form - control" type =" number " i d =" Population " name =" Population" value =" 8539000 . 00 " /> Ра бота с эл ементами laЬel Элем ент label трансформируется классом Label TagHelper , которы й исполь зу ­ ет класс модели представления, чтобы удостовериться в том, что метки н е содержат опечаток и являются согласованными . Существует только один поддерживаемы й ат­ рибут, который описан в табл. 24. 7 . Таблица 24. 7. Атрибут встроенного дескрипторного вспомогательного класса для элементов label Имя asp - for Описание Этот атрибут применяется для указания свойства модели представления, которое олицетворяет элемент label Дескрипторный вспомогательный класс будет использовать имя свойства модели представления для установки значения атрибута for и содержимого элемента label . В листинге 24. 13 атрибут asp - for применяется к элементам label формы, которы е будут трансформированы дескрипторным вспомогательным классом.
Глава 24 . Использование дескрипторных вспо мо гательных классов для форм 755 Листинг 24. 13. Использование дескрипторного вспомогательного класса в файле Create.cshtml @model City @{ Layout = " Layout "; } <form method= " post " asp - controller="H ome " asp - action= " Create " asp - antiforgery= "t rue"> <div class= " form - group"> <laЬel asp-for="Name"></laЬel> <input class= " form-control " asp-for="Name" /> </div> <div class = " form-group " > <laЬel asp-for="Country"></laЬel> <input class= "f orm - contro l" asp - for= " Country " /> </div> <div class= " form-group " > <label asp-for="Population"></laЬel> <input class= " form - control " asp-for="Population" /> </div> <button type= " submit " class= " btn btn - p rimary " >Add</button> <а class= " Ьtn Ьtn - primary " h ref= " /Home/ Index " >Cancel</a> </form> Поскольку элементы label пусты , дескрипторный вспомогательный класс будет применять им ена свойств модели в качестве содержимого этих элементов и установит атрибут for, который сообщает браузеру о том, с каким элементом input ассоции­ рован каждый элемент label. Если вы запустите приложение, запросите URL вида /Ноте/ Crea te или / Home / Edi t и просмотрите НТМL-разметку, отправленную брау­ зеру. то заметите следующие выходные элементы: <form method= " post " action ="/H ome/ Create" > <div class= " form-group " > <label for="Name">Name</laЬel> < i nput class= "f orm - control " typ e= "text" id="Name" name= " Name " value="London" /> </div> <div class= " form - group " > <laЬel for="Country">Country</laЬel> <input class= " form - control " type= " text" id= " Country " name= " Country " va lue ="UK" /> </div> <div class= "form - group " > <laЬel for="Population">Population</laЬel> <input class= " form - control " type= "number " id="Population" name= " Population" val ue= " 8539000 . 00 " /> </div> <button type= " submit " class= "Ьtn Ьtn - pr im ary " >Add</button> <а class= " Ьtn Ьtn -pr ima ry" hr ef= " /Home/ Inde x " >Cancel</a> </form> Переопределить значени е, используемое как содержимое элемента label, можно путем применения атрибута Display к свойству класса модели (листинг 24.14) .
756 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 24.14. Изменение описания для свойства модели в файле Ci ty. cs using System.ComponentModel . DataAnnotations ; namespace Cit ie s . Models puЫic class City { [Display(Name = "City")] puЫic string Name { get; set; } puЫic string Country { get ; set ; [ DisplayFormat(DataFo r matString = " {O : F2} ", App l yFormatinEditMode = t rue)] p uЬli c int? Population { ge t; se t; Аргумент Name указывает значение для использования вместо имени свойс­ тв а . Запустив приложение , запросив URL вида /Home/Create и заглянув в НТМL­ разметку , которая отправлена браузеру , вы обнаружите, что содержимое элемент а label и з менилось: <div class="form- group"> <laЬel for="Narne">City</laЬel> <input class=" form- control" type=" text " i d= "Name " name= "Name " value=" London" /> </div> Обратите внимание, что значение атрибута for не изменилось , так что браузеру известно об ассоциировании эл емента label со специфическим эл ементом input, н а который атрибут Display не оказыва ет воздействия . Совет. Вы можете воспрепятствовать установке дескрипторным вспомогательным классом содержимого элемента label, определив его самостоятельно . Это полезно, если вы хо­ тите , чтобы элементы labe l содержали нечто большее, чем просто имя свойства, кото­ рое способен обеспечить встроенный дескрипторный вспомогательный класс . Работа с элементами select и option Элем енты select и option применяются для предоставления пользователю фик­ сированного набора вари антов в место открытой з апис и данных, которая возможна посредством элемента inpu t . Класс SelectTagHelper отвечает за трансформиров а­ ние эл емент ов select и поддерживает атрибуты , опис ан ные в табл . 24.8. Таблица 24.8. Атрибуты встроенного дескрипторного вспомогател ь ного класса для элементов select Имя asp- for asp-iterns Описание Этот атрибут используется для указания свойства модели представления, которое олицетворяет элемент select Этот атрибут применяется, чтобы указать источни к значений для элемен­ тов option, содержащихся внутри элемента se l ect
Глава 24. Использование дескрипторных вспомогательных классов для форм 757 Атрибут asp-for устанавливает значения атрибутов for и id, чтобы отразить свойство модели, которое он получает. В листинге 24.15 элемент input для свойства Country заменен элементом select , определяющим атрибут asp -for . Листинг 24.15. Использование элемента select в файле Create. cshtml @model City @{ Layout = "_L ayout "; } <form method= "post " asp-controller= "Home" asp-action="Create" asp - antiforgery= " true " > <div class= " form - group " > <label asp - for= " Name " ></label> <input class= "form - control " asp -fo r="Name" /> </div> <div class= " form -g roup " > <label asp-for="Country"></label> <select class="form-control" asp-for="Country"> <opti.on di.saЬl.ed sel.ected val. ue=" ">Sel.ect а Country</opti.on> <option>UK</option> <opti.on>USA</opti.on> <option>France</option> <option>China</option> </select> </div> <div class= " form-group " > <label asp - for= " Population " ></label> <input class= " form-control" asp - for =" Population" /> </div> <button type = " submit " class= "Ьtn Ьtn-primary">Add</button> <а class= " btn Ьtn - primary " href=" /Home/Index">Cancel</a> </form> Здесь элемент select вручную заполняется элементами option , которые пред­ лагают пользователю для выбора диапазон стран. Запустив приложение и запросив URL вида /Home/Create , вы заметите, что НТМL-размет1щ, отправленная клиенту, содержит следующий элемент select: <sel.ect cl.ass="f'orm-control." i.d="Country" name="Country"> <option disaЫed selected value="">Select а Count ry</option> <option>UK</option> <option>USA</option> <option>France</option> <option>China</option> </select> Если вы запросите URL вида /Home/Edit и просмотрите НТМL-разметку, послан­ ную браузеру. то обнаружите, что значение свойства Country объекта модели было применено для изменения выбранного элемента option: <select class= "form -control " id="Country" name="Country"> <option disaЫed selected value="">Select а Country</option> <opti.on selected="selected">UК</option> <option>USA</option> <option>France</option> <option>China</option> </s e lect>
758 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Задача выбора элемента option решается Юiассом OptionTagHelper, ноторый получает инструкции от SelectTagHelper через коллекцию TagHelperContext . Items . Как объяснялось в главе 23, эта коллекция используется дескрипторными вспомогательными классами, которым нужно работать вместе. Данные, добавляемые в коллекцию Items классом SelectTagHelper, будут задействованы в следующем разделе при создании специального дескрипторного вспомогательного класса, пред­ назначенного для обхода ограничения встроенного класса. Использование источника данных для заполнения элемента select Явное определение элементов option для элемента select - удобный подход для вариантов выбора, которые всегда имеют одни и те же значения. Однако он ничем не поможет в ситуации, когда необходимо предоставить варианты, берущиеся из данных модели, или когда нужен одинаковый набор вариантов в нескольких представлениях, но нежелательно вручную поддерживать дублированное содержимое. Генерирование элементов option из перечисления Если есть фиксированный набор вариантов для предоставления пользователю, но вы не хотите дублировать его в представлениях повсюду в приложении, тогда можете применить перечисление. Добавьте в папку Mode l s файл н:ласса по имени Co untryNames. cs и определите в нем перечисление, как показано в листинге 24.16. Листинг 24.16. Содержимое файла CountryNames. cs из папки Models narnespace Cities.Models { puЬlic enurn CountryNarnes UK, USA , France , China Использовать перечисление в атрибуте asp - i tems напрямую нельзя, т.н. дескрип ­ торный вспомогательный класс ожидает получить последовательность объектов SelectListltem. Тем не менее, доступен удобный вспомогательный метод, который выполняет требуемое преобразование (листинг 24. 17). Листинг 24.17 . Применение перечисления для генерации элементов option в файле Create . cshtml @model City @{ Layout = 11 Layout 11 ; } <form method= 11 post 11 asp - controller= 11 Home 11 asp - action= 11 Create 11 asp-antiforgery= 11 true 11 > <div class= " form-group 11 > <label asp-for="Name 11 ></label> <input class= 11 forrn - control " asp - for= 11 Name" /> </div> <div class= " form-group " > <label asp-for= 11 Country 11 ></label>
Глава 24. Использование дескрипторных вспомогательных классов для форм 759 <select class="form-control" asp-for="Country" asp-items="@new SelectList(Enum.GetNames(typeof(CountryNames)))"> <option disaЫed selected value=" ">Select а Country</option> </select> </div> <div class="form-group"> <label asp-for= " Population"></label> <input class= " form - control" asp - for="Population" /> </div> <button type="submit" class="Ьtn Ьtn-primary">Add</button> <а class="Ьtn Ьtn-primary " href= " /Home/Index">Cancel</a> </form> Когда используется перечисление, лучший способ генерации элементов option предусматривает снабжение атрибута asp-items объектом SelectList, который за­ полнен именами значений перечисления. "За кулисами" класс SelectTagHelper ге­ нерирует элементы option из последовательности IEnumeraЫe<SelectListitem>, а ЮJасс SelectList реаЛизует этот интерфейс. Запустив приложение и запросив URL вида /Home/Create или /Home/Edit, вы заметите, что НТМL-разметка, отправленная браузеру, содержит набор элементов option, которые соответствуют значениям в перечислении: <select class="form-control " id="Country" name= " Country"> <option disaЫed selected value= "" >Select а Country</option> <option>UK</option> <option>USA</option> <option>France</option> <option>China</option> </select> Обратите внимание, что дескрипторный вспомогательный класс оставил один эле­ мент-заполнитель option. Любые элементы option, которые определяются явным образом, остаются на месте, т.е. вам не придется смешивать заполнители и значения данных. Генерирование элементов option из модели Если необходимо генерировать элементы option для отражения данных в модели, то самый простой подход заключается в предоставлении данных, требуемых при ге­ нерации элементов, посредством объекта ViewBag (листинг 24.18). Листинг 24.18. Предоставление данных для выбора через объект ViewBag в файле HomeController. cs using Microsoft.AspNetCore.Mvc ; using Cities . Models; using System . Linq ; using Microsoft.AspNetCore.Mvc.Rendering; namespace Cities.Controllers ( puЬlic class HorneController : Controller private IRepository repository; puЬlic HomeController ( IReposi tory repo) { repository = repo;
760 Часть 11. Подробные сведения об и нфраструктуре ASP.NET Core MVC puЫic ViewResu l t Index() => View(repository . Ci t i e s) ; puЫ i c ViewResu l t Edit() ( ViewBag.Countries = new SelectList(repository.Cities . Select(c => c.Country) .Distinct()); retur n View( "Creat e ", repository . Cities . First( ) ) ; puЬlic ViewResu l t Create() { ViewBag.Countries = new SelectList(repository.Cities . Select(c => c . Country) .Distinct()); retur n View() ; [HttpPost ] [ValidateAntiForgeryToken] puЫic IActionResult Create(City city) reposi t ory . AddCity(city) ; return RedirectToAction( "Index " ) ; Методы действий Edit () и Crea te () устанавливают свойство Vi ewBag . Countries в объект SelectList, который з аполня ется уник ал ьными знач е ниями для свойства City . Country в хранилище . В листинге 24. 19 посредство м атрибута a s p - items дес к рипторному в с помог ательному классу сообщается о необхо дим ости получ е ния данных дл я элементов option из свойства ViewBag . Co u ntries . Листинг 24. 19. Применение объекта ViewBag для заполнения элементов option в файле Create . cshtml @model Ci ty @{ Layout = " Layout "; } <form method= "post" asp- controll er= " Home" asp- action=" Create" asp - antiforgery= " true " > <div class= " form-gro u p " > <label asp- for= " Name " ></l abel> <input class="form-control" asp-for="Name" /> </div> <div class= " form - group" > <labe l asp - for= " Country " ></ l abel> <select class="form-control" asp-for="Country" asp-items="ViewBag.Countries"> <option disaЬled selected value= "" >Se l ect а Co un try</opt i on> </se l ect> </div> <div class= " form - group " > <label asp - for= " Population " ></ l abe l > <input class= " form - contro l" asp -for= " Popul ation " /> </div> <button type= " submit " class= " Ьtn Ьtn - pr im ary " >Add</button> <а class="Ьtn Ьtn-primary" href= " /Home/Index" >Cancel</a> </form>
Глава 24. Исnользование дескриnторных всnомогательных классов для форм 761 Запустив приложение и запросив URL вида /Home/Createor /Home/Edi t, вы об­ наружите, что были созданы такие элементы option: <select class="form- control" id="Country " name="Country"> <op t ion disaЫed selected value="">Select а Country</option> <option selected>UK</option> <option>USA</option> <option>France</option> </select> Использование специального дескрипторного вспомогательного класса для генерации элементов option из модели Проблема с передачей данных для элементов option через объект ViewBag связа­ на с тем, что нужно не забыть генерировать данные в каждом методе действия, кото­ рый визуализирует представление, где применяется дескрипторный вспомогательный класс. Это приводит к дублированию кода, что видно в листинге 24.18, и затрудняет тестирование и сопровождение контроллера. Более эффективный подход заключается в создании специального де­ скрипторного вспомогательного класса, который дополняет встроенный класс SelectTagHelper . Добавьте в папку Infrastructure/TagHelper файл класса по имени SelectOptionTagHelper. cs с определением из листинга 24.20. Листинг 24.20 . Содержимое файла SelectOptionTagHelper. cs из папки Infrastructure/TagHelper using System; using System . Collections.Generic; using System .Linq; u sing System.Reflection ; using System.Threading . Tasks; using Cities . Models; u sing Micro s oft . AspNetCore.Mvc . Rendering; u sing Microsoft.AspNetCore.Mvc.TagHelpers; u sing Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore . Razor.TagHelpers; namespace Cities . Infrastructure.TagHelpers { (HtmlTargetElement("select", Attributes = " model -for")] puЫic class SelectOptionTagHelper : TagHelper { private I Repository repository; puЬlic SelectOptionTagHelper(IRepository repo) repository = repo; puЬlic ModelExpression ModelFor { get; set; } puЫic override async Task ProcessAsync(TagHelperContext context , TagHelperOutput output) output.Content.Append( (await output.GetChildContentAsync(false)) .G etContent() ); object selected; context.Items.TryGetValue(typeof(SelectTagHelper), out selected) ; IEnumeraЫe<string> selectedValues = (selected as IEnumeraЫe<string>) ?? EnumeraЫe . Empty<string>();
762 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Propert yinfo pr oper ty = typeof (Ci ty) . GetTypeinfo() . GetDeclaredProp erty(ModelFor . Name) ; foreach (stri ng country i n r eposi tory . Ci ties . Select(c => prope r ty . Ge t Va l ue(c)) . Di stinct()) if (selectedValues . Any(s = > s . Equals(country , S t ringComparison . Ordina l ignoreCase))) { output. Content . Ap pendHtml($ " <option s elected>{country}</option> " ) ; else { output . Content . AppendHtml ($ " <option>{country}</option> " ) ; Специальный д е скрипторный вспомогательный класс оп е риру ет на эле ме нтах select с атрибутом model - for и использует внедрени е зависимост е й , чтобы полу­ чить объект хранилища, который можно применять для доступа к данным модели независимо от контро лл ера , визуализирующего представ ле ние. В дескрипторно м вспомогательном классе определен асинхронный метод ProcessAsync () , т.к. он уп­ рощает процесс получ ения и сохранения любого существующего соде ржимого эле ­ мента select , что делается посредством метода GetChildContentAsync () . Кл асс SelectTagHe l per указывает имена элементов option , которые должны быть выбраны, через запись в коллекции Items, используя собс т в е нный тип в ка­ честв е нл юч а. Де скрипторный вспомогательный клас с получает с писок выбр анных элементов и применяет его в сочетании с результатами запрос а LINQ при гене рации эле м ентов option для каждого уникального значения в хранилище . В листинге 24.21 элемент selec t был обновлен с целью замены атрибута asp -i tems ат рибутом model - for , а также добавл е но в ыр а ж е ние @addTagHelper, которо е внлюча ет специальный дескрипторный вспомогательный класс только для этого пр едставл е ния. Листинг 24.21. Включение специального дескрипторного вспомогательного класса в файле Create. cshtml @model City @addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities @{ Layout = " Layout "; } <form method= " post " asp - control ler=" Home " asp - action= " Create " asp - antiforgery= " true " > <div class= " form - group " > <label asp- for=" Name " ></l abel> <input class=" form - control " asp- for=" Name " /> </div> <div class= " form - group " > <label asp - for= " Country " ></label> <select class="form-control" asp-for="Country" asp-items="ViewBag.Countries"> <option disaЫed selected valu e = "" >Select а Country</option> </select> </div>
Глава 24 . Использование дескрипторных вспомогательных классов для форм 763 <div class= " form - group " > <label asp-for="Population"></label> <input class= " form-cont rol " asp-for="Population" /> </div> <button type= " submit " c l ass= " Ьtn Ьtn -p rimary " >Add</button> <а class= " btn btn-primary " href= " /Home/Index " >Cancel</a> </form> Новый дескрипторный вспомогательный класс генерирует тот же самый вывод. но делает это без использования данных ViewBag, которые требуются встроенному вспомогательному классу. Такой подход предпочтителен тем, что он сохраняет мето­ ды действий сфокусированными на свои специфические задачи и сберегает общую форму приложения. Работа с элементами textarea Элемент textarea применяется для запроса у пользователя крупного объема текс­ та и обычно используется для неструктурированных данных, таких как комментарии или замечания . Класс TextAreaTagHelper отвечает за трансформирование элемен­ тов textarea и поддерживает единственный атрибут, описанный в табл. 24.9 . Таблица 24.9 . Атрибут встроенного дескрипторного вспомогательного класса для элементов textarea Имя asp- for Описание Этот атрибут применяется для указания свойства модели представления , которое олицетворяет элемент textarea Класс TextAreaTagHelper относительно прост; значение, предоставляемое в ат­ рибуте asp-for, используется для установки атрибутов id и name элемента textarea. Чтобы взглянуть на работу этого дескрипторного вспомогательного класса, добавьте в класс модели Ci ty новое свойство (листинг 24.22). Листинг 24.22 . Добавление свойства в файле Ci ty. cs using System . ComponentModel. DataAnnot a tions; namespace Cities . Models puЬlic c l ass City { [Display(Name = " City " ) ] puЫic string Name { ge t ; set; ) puЬlic string Count ry { get ; set ; [DisplayFormat(Data FormatString = " {O:F2)", ApplyFormatinEditMode = true)] puЫic int? Popu lation { get ; set; } puЫic string Notes { get; set; } Добавьте элемент textarea к представлению Create . cs h tml с применением ат­ рибута asp- for для ассоциирования элемента со свойством Notes класса Ci ty (лис ­ тинг 24.23).
764 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 24.23. Добавление элемента textarea в файле Create. cshtrnl @model City @addTagHelper Cities . Infrastructure . TagHelpers .Sel ectOptionTagHelper , Ci ties @{ Layout = 11 Layout 11 ;} <fo rm method= 11 post 11 asp -controller= 11 Ho me 11 asp-action= 11 Create 11 asp-antiforgery="true 11 > <div class= 11 form-g roup 11 > < label asp-for= " Name " ></label> <i np ut class= 11 form - control " asp - for= 11 Name " /> </div> <div class= 11 form- group"> <label asp - for= "C ountry 11 ></ l abel> < sel ect class= 11 f orm-control 11 asp - for= " Country " asp -items="ViewBag .C ountries " > <o ption disaЬled selected value='"'>Select а Country</option> </select> </div> <div class= 11 form-group 11 > <label asp -f or= " Population " ></lab el > <input class = 11 form- control 11 asp - for= "Pop ulation 11 /> </div> <div class= 11 forrn-group 11 > <laЬel asp-for= 11 Notes"></laЬel> <textarea class= 11 forrn-control 11 asp-for="Notes 11 ></textarea> </div> <button type= 11 submit " class ="Ьt n Ьtn-primary 11 >Add</button> <а class= 11 Ьtn Ьtn-primary 11 href= 11 /Home/Index 11 >Cancel</a> </form> Запустив приложение и запросив URL вида /Ноте/ Create или / Н оте/ Edi t . вы заметите, что НТМL-разметка, отправленная браузеру, включает следующий элемент textarea: <di v cl ass= 11 form- group"> <label for= " Note s">Not es</labe l> <textarea id= 11 Notes 11 narne= 11 Notes 11 ></textarea> </div> Несмотря на простоту, IOiacc TextAreaTagHelper обеспечивает согласованность с остальными дескрипторными вспомогательными классами для форм , которые рас­ сматривались в настоящей главе. Дескрипторные вспомогательные классы для проверки достоверности форм Есть еще два дескрипторных вспомогательных класса, которые имеют отношение к НТМL-формам; ради полноты они описаны в табл. 24.10, но объясняются в главе 27. Эти вспомогательные классы используются для предоставления пользователю откли ­ ка. когда введенные им данные не удовлетворяют ожиданиям приложения.
Глава 24 . Использование дескрипторных вспомогательных классов для форм 765 Таблица 24.1 О. Дескрипторные вспомогательные классы для проверки достоверности Имя Validat ionMessag e ValidationSшrnna ry Резюме Описание Этот дескрипторный вспомогательный класс применяется для предоставления пользователю отклика проверки достоверности, касающегося одиночного элемента формы Этот дескрипторный вспомогательный класс используется для предоставления пользователю отклика проверки достоверности , касающегося всех элементов в форме В данной главе были описаны встро енные дескрипторные вспомогательные клас­ сы, которые применяются для трансформации элементов НТМL-форм . Эти дескрип­ торные вспомогательные классы гарантируют, что формы генерируются напрямую из класса модели, сокращая возможности возникновения ошибок и предлагая согласо­ ванный подход к написанию представлений Razor. В следующ ей главе рассматрива­ ются оставшиеся встроенные дескрипторные вспомогательные классы , которы е име­ ют дело с набором разных НТМL-элементов.
ГЛАВА 25 Использование других встроенных десв:рипторных вспомогательных Rлассов д ескрипторные вспомогательные массы, которые были описаны в главе 24, со­ средоточены на выпуск НТМL-форм, но они не являются единственными встро­ енными дескрипторными вспомогательными массами, предлагаемыми инфраструкту­ рой ASP.NEТ Core MVC. В этой главе рассматриваются дескрипторные вспомогательные массы, которые управляют файлами JavaScript и таблицами стилей CSS. создают URL для якорных элементов, пр едоставляют возможность аннулирования кеша для элемен ­ тов изображений и поддерживают кеширование данных . Вдобавок будет описан де­ скрипторный вспомогательный класс, обеспечивающий доступ для URL, относитель­ ных к приложению, которые помогают гарантировать, что браузеры могут обращаться к статическому соде ржимо му, когда приложение развернуто в среде, разделяемой с другим и приложениями. В табл. 25.1 приведена сводка для настоящей главы. Таблица 25.1 . Сводка по главе Задача Включение содер ж имого на основе среды размещения Выбор файлов JavaScript Использование сети достав­ ки содержимого для файлов JavaScript Выбор файлов CSS Использование сети доставки содержимого для файлов CSS Решение Листинг Используйте элемент environment 25 .1, 25 .2, 25.6 Примените атрибуты asp - src- include и 25 .3 -25.5 asp-src - exclude к элементу script Примените атрибуты asp - fallback к 25.7, 25 .8 элементу script Примените атрибуты asp- href- include 25 .9 и asp- href- excl ude к элементу link Примените атрибуты asp-fallback к 25 .10 элементу link
Глава 25. Использование други х встроенных дескрипторных вспомогательных классов 767 Задача Генерация URL для якорного элемента Обеспечение обнаружения изменений в изображениях Кеширование данных Соэдание URL , относительных к приложению Окончание табл. 25 . 1 Решение Листинг Используйте вспомогательный класс 25. 11 AnchorTagHelper Примените атрибут asp- append- version 25.12 к элементу img Используйте элемент cache 25. 13-25.21 Снабдите URL префиксом - 25 .22 -25 .24 Подготовка проекта для примера Мы продолжим работать с проектом Ci ties, созданным в главе 24 . Подготовка для настоящей главы предусматривает создание пап ки wwwroot/ images и добавле­ ние в нее файла изображения по имени ci ty . png . В нем находится общедоступная фотография панорамы Нью-Йорка (рис . 25.1) . Рис. 25. 1. Добавление изображения в проект Файл изображения ci ty . png входит в состав кода примеров для книги, доступ­ ного для загрузки на веб-сайте издательства. При желании можете воспользоваться собственным изображением . Еще одно изменение, которое потребуется сделать, связано с добавлением в проект пакетаjQuегу (листинг 25.1). Щелкните на кнопке Show All Files (Показать все файлы) в верхней части окна Solution Explorer, чтобы отобразился файл bower . j son. Листинг 25.1 . Добавление пакета jQuery в файле bower. j son "name ": "asp . net ", " private ": true , " dependencies ": { "bootstrap": "3 .3 .6", "jquery": "2. 2 . 4" Запустив приложение, вы увидите список объекто в в хранилище и смож ете созда­ вать новы е объекты (рис. 25.2) .
768 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC х · POpulalion City ~ · С LФ -i~~-;;~~7711н;;~;~~~.-------·--QJ : ! ·- ---·- -·--"'==·'"='·"'==--::-:===-с=~"==-·-----·-·11, :1 8 ,539 .000 11 8,406. ООО :.·.•! 998, 537 2 ,244 ,000 ii london uк No1vYork USA San Jose USA Select а Cou11try Paris France Popul ati on - •i i 1 L-~-------- !\ Notes ~ А ;1 111• ! "____,L_______________________ J Рис. 25.2 . Выполнение примера приложения Использование вспомогательного класса для среды размещения Класс EnvironmentTagHelper применяется к специальному элементу environment и определяет, включается ли область содержимого в НТМL-разметку, отправляемую браузеру, на основе среды размещения, как было показано в главе 14. Это может не казаться самым захватывающим местом, чтобы начинать с него, но данный вспомога­ тельный класс необходим для эффективного использования ряда связанных средств, которые будут описаны позже. Элемент environment полагается на атрибут names, который описан в табл. 25.2 для ссьшки в будущем. Таблица 25.2 . Атрибут встроенного дескрипторного вспомогательного класса для элементов environment Имя names Описание Этот атрибут применяется для указания списка разделенных запятыми имен сред размещения, для которых содержимое, находящееся внутри элемента environment, будет включаться в НТМL-разметку, отправляемую клиенту Добавьте в разделяемую компоновку элементы environment, включающие разное содержимое в представление для среды разработки и производственной среды (лис­ тинг 25.2). Листинг 25. 2 . Использование элементов environmen t в файле _ Layout. csh tml < ! DOCTY PE html> <html > <head> <meta name=" viewpo r t " content=" width=device - width " />
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 769 <title>Cities</title> <link href="/liЬ/bootstrap/dist/css/bootstrap.css" rel= " stylesheet" /> </head> <body class= "pane l- body " > <environment names="development"> <div class="panel-body bg-info"><h2>This is Development</h2></div> </environment> <environment names="production"> <div class="panel-body bg-danger"><h2>This is Production</h2></div> </environment> <d i v>@Re n derBody()</div> </body> </html> На рис. 25.3 показаны результаты запуска приложения в среде разработки и про­ изводственной среде. Элемент environment проверяет имя теriущей среды размеще­ ния и либо включает имеющееся в нем содержимое, либо пропускает его (сам элемент environment всегда опускается в НТМL-разметке, отправляемой клиенту). 1 ! +- ·)> С . [J locallюs t:62495/Home/ ,---·· - -----· ---- ---- ! 1 Loodoo UK Loodon UK 1 New York USA New ,УШ/( USA ~ --·r "~ r-#,__. ~·- -.#r...,.....,.~ .~ Рис. 25.3 . Управление содержимым с применением среды размещения Использование вспомогательных классов для JavaScript и CSS Следующая категория встроенных вспомогательных классов применяется для уп­ равления файлами JavaScrlpt и таблицами стилей CSS посредством элементов script и link, которые обычно вюrючаются в разделяемую компоновку. Как будет показано в последующих разделах, эти дескрипторные вспомогательные классы являются мощ­ ными и гибкими , но требуют пристального внимания во время использования, чтобы избежать создания непредсказуемых результатов. Управление файлами JavaScript Класс ScriptTagHelper - это встроенный дескрипторный вспомогательный класс для элементов script. Он предназначен для управления включением файлов JavaScrlpt в представления с применением атрибутов, которые кратко описаны в табл. 25.3 и подробно рассматриваются далее в главе.
770 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 25.З. Атрибуты встроенного дескрипторного вспомогательного класса для элементов script Имя asp-src -include asp- src - exclude asp- append- version asp-fallback- src Описание Этот атрибут используется для указания файлов JavaScript, которые будут включены в представление Этот атрибут применяется для указания файлов JavaScript, которые будут исключены из представления Этот атрибут используется для аннулирования кеша, как описано во врезке " Понятие аннулирования кеша" далее в главе Этот атрибут применяется для указания запасного файла JavaScript, подлежащего использованию в случае возникно­ вения проблемы с сетью доставки содержимого asp- fallback- src - include Этот атрибут применяется для выбора файлов JavaScript, которые будут использоваться при наличии проблемы с сетью доставки содержимого asp- fallback-src - exclude Этот атрибут применяется для исключения файлов JavaScript, чтобы предотвратить их использование в случае возникновения проблемы с сетью доставки содержимого asp-fallback-test Этот атрибут применяется для указания фрагмента JavaScript, который будет использоваться для определения , корректно ли бы загружен код JavaScript из сети доставки содержимого Выбор файлов JavaScript Атрибут asp-src - include применяется для включ ения файлов JavaScript в пред­ ставление с использованием шаблонов универсализации имен, которые поддержива­ ют набор групповых символов, применяемых для сопоставления с именами файлов. В табл. 25.4 описаны наиболее распростран енные шаблоны универсализации имен. Таблица 25.4 . Распространенные шаблоны универсализации имен Шаблон ? * ** Пример js/src? . js js/* . js Описание Этот шаблон соответствует любому одиночному символу кроме /. Пример шаблона соответствует любому файлу из каталога j s с име­ нем, состоящим из src, за которым следует любой символ, а после негорасширение .j s,такомукакjs/srcl.j sиjs/srcX.j s,но неjs/src123.jsилиjs/mydir/srcl.js Этот шаблон соответствует любому количеству символов кроме / . Пример шаблона соответствует любому файлу из каталога j s с именем , которое заканчивается расширением . j s, такому как js/srcl . js и js/srcl23 . js, но не js/mydir/srcl. js j s1**1 * . j s Этот шаблон соответствует любому количеству символов, включая /. Пример шаблона соответствует любому файлу с расширением . j s , который содержится внутри каталога j s или любого его под­ каталога, такому как /js/srcl . j s и /j s/mydir/ srcl .j s
Глава 25. Исnользование других встроенных дескриnторных всnомогательных классов 771 Использование шаблона универсализации имен в атрибуте asp-src -include оз­ начает, что представление будет всегда вЮiючать файлы JavaScript приложения, даже если имя или путь к файлам изменяются либо файлы добавляются или удаляются. В листинге 25.3 демонстрируется выбор файлов JavaScript для пакета jQuery, кото­ рый Воwеrустанавливает в папку wwwroot/liЬ/jquery/dist. Листинг 25.З. Выбор файлов JavaScript в файле_Layout. cshtml <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width " /> <title>Cities</title> <script asp-src-include="/lib/jquery/dist/**/*.js"></script> <link href = " /liЬ/bootstrap/dist/css/bootstrap.css" rel="stylesheet" /> </head> <body clas s ="panel-body"> <div>@RenderBody()</div> </body> </html> В примере применяется распространенный шаблон. Шаблоны оцениваются внут­ ри папки wwwroot, а библиотекаjQuеrу доставляется как одиночный файл JavaScript поимени jquery.j s. Этот шаблон универсализации имен пытается выбрать файл jQuery, одновре­ менно приспосабливаясь к любым будущим изменениям в способе распространения jQuery, таким как изменение имени файла JavaScript. Запустив приложение и про­ смотрев НТМL-разметку, отправленную Юiиенту, вы увидите, что в ней присутствует проблема: <h ead> <meta name="viewport" content= "width=device - width" /> <title>Cities</title> <script src="/lib/jquery/dist/jquery.js"></script> <script src="/lib/jquery/dist/jquery.min.js"></script> <link href="/liЬ/bootstrap/dist/css/bootstrap.css" rel="stylesheet" /> </head> Класс ScriptTagHelper генерирует элемент script для каждого файла, имя кото­ рого соответствует шаблону, указанному в атрибуте asp- src-include. Вместо выбора только файла jquery . js имеется также элемент script для файла jquery.min . j s. который является минифицированной версией файла j query . j s и будет использо­ ваться большинством приложений, поскольку он содержит тот же самый код, выра­ женный в более компактной, но менее читабельной манере. Вы можете даже не подоз­ ревать, что дистрибутив jQuery вЮiючает минифицированный файл, т.к. Visual Studio его по умолчанию скрывает. Чтобы отобразить полное содержимое папки wwwroot/ liЬ/jquery/dist, понадобится раскрыть элемент jquery. j s в окне Solution Explorer и затем сделать то же самое с элементом j query. min. j s (рис. 25.4).
772 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Solut ion Explorer " ,..:~=-.;.Х.;_,__ ______ __--1 So lution Explorer "~Х (1) 1•0 • ~ @ !iiJ) 1~ Sol ution Explorer ~.;:,} _GJ1•0 " ~-~ ~@_J_~ ~-1 ~ -~1·~~ ~=~"'~ 1® I~: P- Search Solulio n Explor" (Ctrl+ ;) - .. ..... ... . - $ wwwroot 1> in1ages ,,, ~ lib 1> llili bootstrap " !jquery " '*1 dist .[] 1> U external 1> src lJ .bower.j son ~ AUTH ORS.txt !J bower.json [ii'! LICENSE.txt ~ README.md Search Solution!_><P!_orer (Ct rl+;) &· ww.чroot 1> images ... ;,;. · lib 1> ~ boot$trap " ""'' jquery ... fio,. 1 dist " .!J jquery.j s . ti. 1> ~ external 1> DJi src !J .bower.js on li!J AUTH ORS. txt !J bower.json [Н1 LIC ENSE .txt 11iJ REA DME. md Qll ' •-юwroot 1> til imag es " . lib 1> • bootst.rap " '°"' j query " ". dist " !J jquery.j s " tJ jquery.niin.js ! ,,-,. tJ• 1> ~ extern al 1> src !J .bo wer.j son l!i') AUT H ORS.Ь<t 6J' bo wer .json (йЬ LICENSE.txt 1EJ README.md Рис. 25.4 . Отображение полного содержимого папки в окне Solutioп Explorer Шаблон , примененный в листинге 25.3, вызвал двукратную отправку браузеру кода JQuery, как в исходной, так и в минифицированно й форме, ч т о приводит к из­ лишней трате полосы пропускания и замедлению работы приложения. В случае не­ которых библиотек результатом могут стать ошибки или непредсказуемое поведение. Решить проблему можно тремя способами, которые рассматриваются в последующих разделах. Использование отображений на исходный код Файлы JavaScript минифицируются с целью уменьшения размеров, что означает возмож­ ность их доставки клиенту быстрее и с меньшим расходом полосы пропускания . Процесс минификации удаляет из файла все пробельные символы, а также переименовывает фун­ кции и переменные так, что значащие имена вроде myHe l pfullyNamedFun ction будут представлены меньшим количеством символов, скажем, xl . В случае применения отлад­ чика JavaScript браузера для поиска источника проблем в минифицированном коде имена , подобные xl , сделают почти невозможным прохождение по коду. Файл j q u ery . min . map является отображением на исходный код , которое рядом браузе­ ров используется для того, чтобы помочь в отладке минифицированного кода , предоставляя соответствие между минифицированным кодом и читабельным полным исходным кодом. На момент написания книги отображения на исходный код не были универсально поддержи­ ваемым средством, но их можно было применять в большинстве последних версий брау­ зеров Chrome и Edge . Например, браузер Chrome будет автоматически запрашивать файл отображений на исходный код, если открыто окно инструментов разработчика, а это значит, что браузеру можно отправлять минифицированные версии файлов JavaScript и по-прежне­ му иметь возможность их легкой отладки.
Глава 25. Использование других встроенных дес кри пторны х вспомогательных классов 773 Сужение шаблона универсализации имен Многие пакеты предоставляют обычные и минифицированные версии своих фай­ лов JavaScript, и если вы собираетесь использовать только минифицированные вер­ сии, тогда можете ограничить набор файлов, которые буlJУт соответствовать шаблону универсализации имен (листинг 25.4). Это хороший подход, когда вы не планируете заниматься отладкой библиотеки jQuery, которая реализована эффективно и не вы­ зывает особых проблем, или же знаете, что целевые браузеры поддерживают отобра­ жения на исходный код. Листинг 25.4. Выбор только минифицированных файлов JavaScript в файле _Layout. cshtml < ! DOCTYPE html> <html> <head> <meta name= " viewport" content= " width=device-width" /> <title>Cities</title> <script asp-src -include="/lib/jquery/dist/**/* . min.js"></script> <link href= " /liЬ/bootstrap/dist/css/bootstrap. css " rel= " stylesheet " /> </head> <body class= " panel - body " > <div>@RenderBody()</div> </body> </html> Запустив приложение и просмотрев НТМL-размет~tу, отправленную браузеру, вы заметите, что был включен только минифицированный файл библиотеки jQuery: <head> <meta name = "viewport " content= " width=device-width " /> <t itle>Cities </title> <script src= "/lib/jquery/dist/jquery.min.js"></script> <link href= " /liЬ/bootstrap/dist/css/bootstrap . css" rel="stylesheet" /> </head> Исключение файлов Применять обычные версии файлов JavaScript без их минифицированных версий труднее , потому что шаблоны универсализации имен усложняют исключение файлов. К счастью, с помощью атрибута asp - src - exc lude можно удалять файлы из списка, построенного атрибутом asp - src -include, как показано в листинге 25.5 . Листинг 25.5 . Исключение файлов JavaScript в файле_Layout. cshtml < ! DOCTYPE html> <html> <head> <meta name= " viewport " content= " width=device-width" /> <title>Cities</titl e> <script asp-src-include="/lib/jquery/dist/**/*.js" asp-src -exclude="**.min.js"> </script> <link href= " /liЬ/bootstrap/dist/ css/bootstrap . css" rel= " stylesheet" /> </head>
774 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Со ге MVC <body class=" panel - body" > <d i v>@Re nderBody()</div> </body> </html > Если вы запустите приложение и заглянете в НТМL-разметку. посл анную браузеру, то увидите, что была включена только обычная версия файла JavaScript: <head> <meta name= " viewport " content= " width=device-width 11 /> <title >Cities</ title> <script src="/lib/jquery/dist/jquery.js"></script> <link href= 11 /liЬ/bootstrap/dist/css/boo t strap . css 11 rel= " s tylesheet 11 /> </head> Использование среды размещения для выбора файлов Распространенный подход предусматривает применение обычных файлов JavaScript на этапе разработки, что облегчает отладку, и использование минифициро­ ванных файлов в производственной среде для сокращения расхода полосы пропуска­ ния . В таком случае можно задействовать элемент environment, чтобы избирательно включать элементы script , основываясь на среде размещ е ния (листинг 25.6). Листинг 25.6 . Применение среды размещения для выбора файлов в файле _ Layout. cshtml < ! DOC TYPE html> <html> <head> <meta name= " viewport " content= "width=devi ce - width " /> <titl e>Cit ies </title> <environment names="development"> <script asp-src-include="/lib/jquery/dist/**/ *. js" asp-src-exclude="**.min.js"> </script> </environment> <environment names="staging, production"> <script asp-src -include="/lib/jquery/dist/**/*.min . js"></script> </environment> <link href= " /liЬ/bootstrap/dist/ css /bootstrap . css " rel= " stylesheet 11 /> </head> <body class= 11 panel -body " > <div>@RenderBody()</div> </body> </html > Пр еимуществ о такого подхода в том, что он адаптирует прилож ени е к среде р аз­ мещения. но при этом придется писать и сопровождать многочисленные наборы эле­ ментов script.
Глава 25. Использование других встроенных дескрипторных вспо м огательных классов 775 Понятие аннулирования кеша Статическое содержимое, такое как изображения, таблицы стилей CSS и файлы JavaScript, часто кешируется, чтобы предотвратить попадание на серверы приложений запросов к редко изменяющемуся содержимому. Кеширование делается разными способами: браузер может сообщить серверу о необходимости кеширования содержимого, приложение может использовать серверы кешей как дополнение к серверам приложений или содержимое мо­ жет распространяться с использованием сети доставки содержимого. Не все кеширование будет находиться под вашим контролем. Например, в крупных корпорациях кеши часто ус­ танавливаются для снижения требований к полосе пропускания, поскольку значительный процент запросов направляется к одним и тем же сайтам или приложениям. С кешированием связана одна проблема: клиенты не получают новые версии статических файлов немедленно после их развертывания, потому что запросы клиентов продолжают обслуживаться с применением ранее кешированного содер ж имого. В конце концов, время существования кешированного содержимого истечет и начнет использоваться новое содер­ жимое , но остается промежуток, когда динамическое содержимое, генерируемое контрол­ лерами приложения, не согласовано со статическим содержимым, доставляемым кешами. В зависимости от содержимого, которое было обновлено, могут возникнуть проблемы с компоновкой или непредсказуемое поведение приложения. Упомянутая проблема решается с помощью аннулирования кеша. Идея состоит в том, чтобы позволить кешам обрабатывать статическое содержимое, но незамедлительно отражать лю­ бые изменения, которые произошли на сервере. Дескрипторные вспомогательные классы поддерживают аннулирование кеша за счет добавления к URL для статического содержимо­ го строки запроса , которая включает контрольную сумму, действующую в качестве номера версии. Скажем , для файлов JavaScript класс ScriptTagHelper поддерживает аннулиро­ вание кеша через атрибут asp - append- version : <script asp - src - include= " /liЬ/jquery/d is t/**/*.min . js " asp-append-version="true" > </script> Включение средства аннулирования кеша приводит к появлению в НТМL-разметке, отправ­ ляемой браузеру, элемента следующего вида : <script src= " / li b/jquery/dist/jquery . min .js?v=ЗzRSQlHF-ocUiVcdv9yKTXqM" > </script> Тот же самый номер версии будет применяться дескрипторным вспомогательным клас­ сом до тех пор , пока вы не измените содержимое файла, например, обновив библиотеку JavaScript , из - за чего будет вычислена другая контрольная сумма . Добавление номера вер­ сии означает, что всякий раз, когда изменяется файл , клиент будет запрашивать отлича­ ющийся URL , трактуемый кешами как запрос для нового содержимого, который не может быть удовлетворен с использованием ранее кешированного содержимого, поэтому он пе­ редается серверу приложений. Затем содержимое кешируется как обычно до следующего обновления, которое приведет к генерации еще одного URL с другим номером версии.
776 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Работа с сетями доставки содержимого Сеть доставки содержимого (content delivery network - CDN) при ме ня ется для пе­ редачи запросов к сод е ржим ому приложения се рверам , которые нах одят ся поблизос­ ти от пользователя. Вместо запрашивания файла JavaScript у ваших серверов браузер запрашивает его у имени хоста, которое преобразуется в географически местный сер­ вер. что уменьшает время загрузки файлов и сокращает ширину полосы пропускания, необходимую приложению. При наличии крупной географически распределенной базы пользователей может оказаться целесообразной коммерческая подписка на CDN, но даже небольшое и простое приложение способно извлечь выгоду от использования бесплатных CDN, приводимых в действие ведущими технологическими компаниями для доставки общих пакетов JavaScript, таких как jQuery. В настоящей главе будет применяться сеть Microsoft CDN, которая предлагает бесплатный доступ I< популярным пакетам, со списком 1<0торых можно о з накомиться по адресу www . asp . ne t /а j ах/ cdn . Здесь используется пакет jQuery 2.2 .4, и вот три URL в сети Microsoft CDN, у~шзывающие на этот выпуск: • http : //ajax.aspnetcdn . com/ajax/jQuery/jquery- 2 . 2 . 4 . js • http://ajax . aspnetcdn . com/aj ax/jQuery/jquery- 2 .2 .4 .min .js • http : //ajax . aspnetcdn . com/a j ax/jQu ery/jquery- 2 . 2 . 4 . min .map Ук аза нны е URL пр едоставляют обычный Jav aScript, минифицированный файл JavaScript и отображение на исходный код для минифицированного файла. В лис­ тинге 25. 7 локальные файлы заменены минифицированным файлом, получаемым из CDN . Листинг 25.7 . Применение CDN в файле _Layout.cshtml < ! DOCTYPE html> <html> <head> <meta name= " viewport " content= "width=d evi ce - wi d t h " /> <ti tle>C i ties< / title> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery- 2 .2 .4 .min.js"> </script> <link h r ef=" / liЬ/bootstrap/dist/css/bootstrap . css " rel= " styl e sheet" /> </head> <body class= " panel - body " > <div>@RenderBody()</div> </body> </html > Указание CDN означает, что никакие запросы для jQuery не будут попадать на сер­ веры приложения . Пробл ема с сетями CDN связана с тем, что они не находятся под контролем вашей организации , т.е. они могут отказывать, оставляя приложение фун­ кционирующим, но неспособным работать ожидаемым образом из- за недоступности содержимого CDN. Чтобы решить проблему, JUiacc ScriptTagHelper предлагает воз­ можность обратиться за помощью к локальным файлам, когда клиент не может загру­ зить содержимое из CDN (листинг 25.8).
Глава 25. Использование други х встроенных дескрипторны х вспомогательны х классов 777 Листинг 25.8 . Использование обхода загрузки из CDN в файле _Layout. cshtml <!DOCTYPE html> <html> <head> <meta name="view.port " content= " width=device - width" /> <title >Cit ies< /title> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.4.min.js" asp-fallback-src -include="/lib/jquery/dist/**/*.min.js" asp-fallback-test="window.jQuery"> </script> <link href= " / liЬ/bootstrap/dist/css/bootstrap. css" rel="stylesheet " /> </head> <body class ="panel-body"> <div>@R enderBody()</div> </body> </html> Атрибуты asp - fallback - src-include и asp- fallback - src - exclude приме­ няются для выбора и исключения локальных файлов . которые будут использоваться. если сеть CDN не способна доставить файл, указанный посредством обычного атрибу­ та src. Чтобы выяснить, работает ли CDN , с помощью атрибута asp - fallback - test определяется фрагмент JavaScript, который будет оцениваться в браузере. Если фраг­ мент оценивается как false, тогда будут запрашиваться запасные файлы. Запустите приложение и просмотрите НТМL-разметку, которая была отправле­ на клиенту. Вы увидите, что класс ScriptTagHelper взял фрагмент из атрибута asp-fallback-test и применил его для создания е ще одного элемента script : <head> <meta name="viewport " content="width=device-width" /> <title>Cities</title> <script src="http://ajax . aspnetcdn.com/ajax/jQuery/jquery-2.2.4 .min .js"> </script> <script> (window . jQuery 1 1document . wri te ( " \u003Cscript src=\u0022\/lib\/jquery\/dist\/jquery .m in .j s \u0022\u003E\u003C\/script\u003E ")); </sc ript> <link href= " /liЬ/bootstrap/dist/css/bootstrap . css " rel="stylesheet" /> </head> Фрагмент JavaScript, указываемый в атрибуте asp - fallback - test . должен возвращать true , если файл из CDN был загружен. и false в противном случае. Простейший подход обычно предусматривает проверку точки входа в функциональ­ ность. предлагаемую кодом JavaScript. Библиотека jQuery создает функцию по имени j Query на глобальном объекте window, которая и проверяется в листинге 25.8 . Вам понадобится найти эквивалентные тесты для всех файлов, загружаемых из CDN. Важно проверять настройки обхода , т.к. вы не сможете обнаружить их отказ до тех пор, пока сеть CDN не прекратит работу, а пользователи утратят доступ к вашему при­ ложению. Проверить обход проще всего путем изменения имени файла, указанного в атрибуте src, на такое, о котором доподлинно известно, что оно не существует (здесь I< имени файла присоединено слово FAIL). и затем с помощью инструментов <F12>
778 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC просмотреть сетевые запросы, выполняемые браузером . Вы должны увидеть ошибку для файла из сети CDN, за которой следует запрос запасного файла. Внимание! Средство обхода CDN полагается на то, что браузеры загружают и выполняют содержимое элементов script синхронным образом в порядке, в котором они опре­ делены. Существует несколько приемов ускорения загрузки и выполнения сценариев JavaScript за счет того, что они делают процесс асинхронным. Однако такой подход мо­ жет привести к тому, что проверка обхода выполнится перед тем, как браузер извлечет файл из CDN и выполнит его содержимое, в результате порождая запросы для запасных файлов даже при нормальной работе CDN и ставя под сомнение вообще использование CDN . Не смешивайте асинхронную загрузку сценариев со средством обхода CDN. Управление таблицами стилей CSS Класс LinkTagHelper - это встроенный вспомогательный класс для элементов link, который применяется при управлении включением таблиц стилей CSS в пред­ ставление. Класс LinkTagHelper поддерживает атрибуты, описанные в табл. 25 .5, использование которых демонстрируется в последующих разделах. Таблица 25.5. Атрибуты встроенного дескрипторного вспомогательного класса для элементов link Имя asp- href-include asp - href- exclude asp-append-version Описание Этот атрибут применяется для выбора файлов, пред­ назначенных атрибуту href вы ходного элемента Этот атрибут используется для исключения файлов из атрибута href выходного элемента Этот атрибут применяется для включения средс­ тва аннулирования кеша, как описано во врезке "Понятие аннулирования кеша" ранее в главе asp-fallback-href Этот атрибут используется для указания запасного файла на случай возникновения проблемы с сетью CDN asp-fallback-href-include Этот атрибут применяется для выбора файлов, ко­ торые будут использоваться при наличии проблемы с CDN asp-fallback-href-exclude Этот атрибут применяется для исключения файлов из набора, который будет использоваться при нали­ чии проблемы с CDN asp-fallback-href-test-class Этот атрибут применяется для указания класса CSS , который будет использоваться при тестировании работоспособности сети CDN asp-fallback-href- test- property Этот атрибут применяется для указания свойства CSS , которое будет использоваться при тестирова­ нии работоспособности сети CDN asp-fallback-href-test-value Этот атрибут применяется для указания значения CSS, которое будет использоваться при тестирова­ нии работоспособности сети CDN
Глава 25. Использование других встроенных дескрипторных вспомогательны х классов 779 Выбор таблиц стилей Класс LinkTagHelper разделяет с классом ScriptTagHelper многие средства, в том числе поддержку шаблонов универсализации имен для выбора либо исключения файлов CSS, так что их не нужно указывать по отдельности. Наличие возможности точного выбора файлов CSS имеет такую же важность, как и для файлов JavaScript, поскольку таблицы стилей тоже поступают в виде обычных и минифицированных версий, дополнительно поддерживая отображения на исходный код. Популярный па­ кет Bootstrap. который применялся для стилизации НТМL-элементов повсеместно в данной книге. включает свои таблицы стилей CSS в папке wwwroot/liЬ/bootstrap/ dist/ css. и если вы раскроете все элементы в окне Solution Explorer, то заметите, что доступны четыре файла (рис. 25.5). Файл boostrap.css является обычной таблицей стилей, файл boostrap.min. css - ее минифицированной версией, а файл bootstrap. css .map - отображением на исходный код. В других файлах находятся стандартная тема Bootstrap, ее обычная и минифицированная версии, а также отображение на исходный код. В листинге 25.9 атрибут asp-href - include используется для выбора минифицированной таблицы стилей. (Кро ме того, удален элемент script для загрузки библиотекиjQuеrу, которая больше не требуется. ) Листинг 25.9 . Выбор таблицы стилей в фaйлe _Layout.cshtml <!DOCTYPE html> <html> <head> <meta name= "viewport " content= " width=dev i ce - width " /> <title>Cities</title> <link asp-href-include="/liЬ/Ьootstrap/dist/**/.min.css" rel="stylesheet"/> </head> <body class="panel - body"> <div>@RenderBody()</div> </body> </html> Здесь должно уделяться аналогичное внимание деталям, как и при выборе файлов JavaScript, потому что довольно легко сгенерировать элементы link для нескольких версий того же самого файла или файлов, которые не нужны. Чтобы управлять выби­ раемыми файлами, можно следовать тем же трем подходам , которые были описаны для файлов JavaScript в предыдущем разделе: сужать шаблон универсализации имен, исключать файлы с применением атрибута asp - href - exclude и использовать эле­ мент environment для выбора между дублирующимися наборами файлов. Работа с сетями доставки содержимого Вспомогательный класс LinkTag предоставляет набор атрибутов для возвраще­ ния к локальному содержимому. когда сеть CDN не доступна, хотя процесс проверки, загрузилась ли таблица стилей, чуть сложнее, чем подобная проверка в отношении файла JavaScript. В листинге 25.10 применяется URL сети MaxCDN для библиоте­ ки Bootstrap. просто чтобы продемонстрировать альтернативу платформе Microsoft (M axCDN - это сеть CDN. рекомендуемая проектом Bootstrap).
780 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 25.10. Использование CDN для загрузки таблицы стилей CSS в файле _Layout. cshtml <!DOCTYPE htrnl> <htrnl> <head> <rneta narne= " viewport " content= "width=device-width " /> <ti t le>C i ties</title> <link href= "https://maxcdn.bootstrapcdn . com/bootstrap/3.3 .6/css/bootstrap . min.css" asp-fallback-href-include="/lib/bootstrap/dist/**/*.min.css" asp-fallback-test-class="btn" asp-fallback-test-property="display" asp-fallback-test-value="inline-Ыock" rel= "s tylesheet " /> </head> <body class= " panel-body " > <di v>@Rend erBody() </div> </body> </htrnl> В ат риб уте href указывается URL сети CDN, а с помощью атрибута a sp- fa llback- href-include выбирается файл, который будет прим еняться, если сеть CDN окажется недоступной. Тем не менее, проверка. работает ли сеть CDN. тре­ бует использования трех разных атрибутов и понимания классов CSS, которые опре­ деляются необходимой таблицей стилей CSS. Средство перехода на запасные таблицы стилей CSS инициируется путем добавле­ ния к отправляемому документу элемента meta для класса, определенного в атрибуте asp - fallback - test - class . В листинге 25.10 был указан класс Ьtn , т.е. к НТМL­ разм етке. отправляемой браузеру. будет добавлен элемент rneta следующего вида: <rneta narne= "x-s tylesheet- fallba ck-test" clas s="btn" /> Указываемый класс CSS должен быть определен в таблице стилей, которая подле ­ жит загрузке из CDN. Заданный здес ь класс Ьtn обеспечивает базовое форматирова ­ ние для кнопочных элементов Bootstrap. Атрибут asp-fallback- test - property применяется для указания свойства CSS, которое устанавливается классом CSS, а атрибут asp - fallback - test - value исполь­ зуется для указания устанавливаемого им значения. Дескрипторный вспомогатель­ ный класс добавляет к представлению код JavaScript, который проверяет значение свойства CSS из элемента meta с целью выяснения, загрузилась ли таблица стилей, и на случай, если не загрузилась. пр едусматривает элементы link для запасных фай­ лов. Класс Ьtn из Bootstrap устанавливает свойство display в inline - Ыock , что является тестом, который проверяет, удалось ли браузеру заг рузить таблицу стилей Bootstrap из сети CDN. Совет. Простейший способ понять, каким образом проводить проверку для сторонних паке­ тов вроде Bootstrap, предполагает применение инструментов <F12> браузера. Чтобы оп­ ределить тест в листинге 25. 1О, элементу назначается класс Ьtn , после чего в браузере инспектируются индивидуальные свойства CSS, которые этот класс изменил. Такой прием проще попытки просмотра длинных и сложных таблиц стилей.
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 781 Работа с якорными элементами Элемент а является базовым инструментом для навигации в рамках приложения и отправки запросов GET приложению с целью получения разнообразного содержимого. Класс AnchorTagH elper используется для трансформации атрибута h ref элементов а , чтобы целевые URL генерировались при участии системы маршрутизации, с при­ м е нением атрибутов из табл. 25.6 . Таблица 25.6 . Атрибуты встроенного дескрипторного вспомогательного класса для якорных элементов Имя Описание asp- action Этот атрибут указывает метод действия, на который будет нацелен URL asp - con trolle r Этот атрибут указывает контроллер , на который будет нацелен URL asp-area Этот атрибут указывает область, на которую будет нацелен URL asp-fragment Этот атрибут используется для указания фрагмента URL (который находится после символа # ) asp - host Этот атрибут указывает имя хоста, на который будет нацелен URL asp- protocol Этот атрибут указывает протокол, который будет применять URL asp- route Этот атрибут указывает имя маршрута, который будет использоваться для генерации URL asp- route- * Атрибуты с именами, начинающимися на asp- rout e -, применяются для указания дополнительных значений, относящихся к URL, так что атрибут a sp- rout e-id используется для предоставления системе маршрутизации значения сегмента id Класс AnchorTagHelper прост и предсказуем: он облегчает генерацию URL в эле­ ментах а , которы е задействуют конфигурацию маршрутизации приложения . В лис ­ тинге 25. 11 эл емент а внутри представления Index . cshtml обновлен, чтобы его ат­ рибут href выпускался дескрипторным вспомогательным классом. Листинг 25.11. Трансформация якорного элемента в файле Index. cshtml @model I E n umeraЫe<City> @{ Layout = " Layout"; ) <tаЫе class="taЫe taЬle-condensed taЬle-bordered"> <thead class="bg-primary"> <tr> <th>Name</th> <th>Country</th> <th class="t ext - right " >Populati o n</th> </tr> </thead> <tbody> @foreach (var city in Model) <tr > <td>@city .N am e </td> <td>@city. Country</td> <td class= " text- right">@city.Populati on? .ToStri ng( "# , ### " )</td> </tr>
782 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Соге MVC </tbody> </tаЫе> <а asp-action="Create" class="Ьtn Ьtn-primary">Create</a> Запустив прилож ение и запросив URL вида /Horne/ I ndex , вы з а м етите , что д е ­ скрипторный вспомогат ельный класс трансформировал эл еме нт а следующим образом: <а c l a s s =" Ьt n Ьtn-p rim ary " href="/Home/Create" >Create</a> Работа с элементами i mq Класс IrnageTagHe lpe r позво ля ет предоставить ср едство аннулирования кеш а для изображений через атрибут src элементов img, давая прилож ению возмо ж ность применять в своих интересах ке ширование и гар а нтируя н е м едл е нное отр аж е ние любых изменений в изображениях. Класс Image TagHelpe r оперирует с элементами i mg, которые определяют атрибут asp - a ppendversion, опи санн ый в та бл . 25. 7 . Таблица 25.7. Атрибут встроенного дескрипторного вспомогательного класса для элементов img Имя asp- append- versi on Описание Этот атрибут используется для включения аннулирования кеша, как объяснялось во врезке "Понятие аннулирования кеша" ранее в главе В листинге 25.12 к разделяемой компоновке добавлен элемент irng с изображени е м панорамы Нью-Й орка, которое было включено в проект в начале главы. (Кроме того, ради простоты обеспечено применение локальных файлов с таблицами стилей.) Листинг 25.12. Добавление изображения в файле _ Layout. cshtml <!DOCTYPE html> <html> <head> <meta n a me= " viewpor t" content = " wi dth=device - widt h" /> <t itl e >Cities</t i tle> <link asp-href-include=" /l iЬ/bootstrap/dist/**/* . min. css " rel= " stylesheet"/> </head> <body class=" panel - body" > <img src="/images/city.png" asp-append-version="true" /> <div>@RenderBody()</di v> </body > </ h tml> Запустив приложение , вы увидите , что изображение появляется в верхней части каждо й страницы . Если вы прос м отрите НТМL-разм етку, которая была отправле на браузеру, то заметите, что URL, используемый для запроса файла изображения , со­ держит контрольную сумму версии: <img src= " /images/city . png ?v=KaМNDSZFЬzNpE8PkЬЗOEXcAJufRcRDpKhOK_IIPNc7E" />
Глава 25. Использование других встроенных дескрипторных вспомогательных классов 783 Как и со средствами аннулирования кеша для файлов JavaScript и таблиц стилей CSS, контрольная сумма, включенная в URL, остается неизменной до тех пор, пока файл не будет модифицирован . И с п ользо в ание кеша данных Инфраструктура MVC поддерживает кеш в памяти, который может применяться для кеширования фрагментов содержимого с целью ускорения визуализации пред­ ставлений. Содержимое, подлежащее кешированию, обозначается в файле пред­ ставления с использованием элемента c ac he, который обрабатывается классом Cach e Ta gH e lpe r через атрибуты, описанные в табл. 25.8. На заметку! Кеширование - удобный инструмент для многократного использования раз­ дела содержимого, чтобы его не приходилось генерировать при каждом запросе. Но эф­ фективное применение кеширования требует тщательного обдумывания и планирования. Хотя кеширование способно улучшить производительность приложения, оно также созда­ ет необычные эффекты, такие как получение пользователями устаревшего содержимого, наличие множества кешей с разными версиями содержимого и нарушение работы раз­ вертываемых обновлений из-за того , что содержимое, кешированное из предыдущей вер­ сии приложения, смешалось с содержимым из новой версии. Не включайте кеширование, если только не имеете дело с четко определенной проблемой низкой производительности, и удостоверьтесь в том, что хорошо понимаете влияние, которое оно будет оказывать . Таблица 25.8 . Атрибуты встроенного дескрипторного вспомогательного класса для элементов cache Имя enaЫed ex pires - on expi res-after expires - sliding vary- by- header vary-by - query Описание Этот атрибут типа bool используется для управления тем, будет ли кешироваться содержимое элемента c ache . Отсутствие дан­ ного атрибута означает включение кеширования Этот атрибут применяется для указания абсолютного времени, когда срок существования кешированного содержимого истечет, в виде значения DateTime Этот атрибут используется для указания относительного вре­ мени , когда срок существования кешированного содержимого истечет, в виде значения TimeSpan Этот атрибут применяется для указания промежутка времени с момента последнего использования, когда срок существо­ вания кешированного содержимого истечет, в виде значения Ti meSpan Этот атрибут применяется для указания имени заголовка за­ проса, который будет использоваться при управлении разными версиями кешированного содержимого Этот атрибут применяется для указания имени ключа строки за­ проса, который будет использоваться при управлении разными версиями кешированного содержимого
784 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Имя vary- by-route vary- by- cookie vary-by -user vary- by priority Окончание табл. 25.8 Описание Этот атрибут применяется для указания имени переменной маршрутизации, которая будет использоваться при управлении разными версиями кешированного содержимого Этот атрибут применяется для указания имени сооkiе-набора , который будет использоваться при управлении разными версия­ ми кешированного содержимого Этот атрибут типа bool применяется для указания, будет ли имя аутентифицированного пользователя использоваться при управ­ лении разными версиями кешированного содержимого Этот атрибут оценивается для предоставления ключа, приме­ няемого при управлении разными версиями кешированного содержимого Этот атрибут используется для указания относительного приори­ тета, учитываемого в случае исчерпания пространства кеша в памяти и очистки кешированного содержимого, срок существо­ вания которого еще не истек Чтобы посмотреть, как работают атрибуты кеша, создайте папку Cornponents, до­ бавьте в нее файл класса по имени TirneViewCornponent. cs и определите компонент представления, приведенный в листинге 25. 13. Листинг 25.13. Содержимое файла TimeViewComponen t. cs из папки Componen ts using Systern; using Microsoft.AspNetCore.Mvc ; namespace Cities.Components { puЬlic class TirneViewComponent : ViewCornponent puЫic IViewComponentResult Invoke() return View(DateTime.Now) ; Метод Invoke () выбирает стандартное представление и передает ему объект DateTirne в качестве модели представления. Для того чтобы обеспечить компонент пр едставления необходимым ему представлением, создайте папку Views/Horne/ Components/Time и добавьте в нее файл представления по имени Defaul t . cshtml с разметкой из листинга 25.14 . Листинг 25.14. Содержимое файла Default.cshtml из папки Views/Home/Components/Time @rnodel DateTime <div class= " panel -body bg-info"> Rendered at @Model . ToString ( "НН: rnm: ss") </div>
Глава 25. Использование других встроенных дескрипторны х вспомогательных классов 785 Объект модели DateTime применяется для отображения текущего времени с точ­ ностью до секунды. В листинге 25.1 5 элемент img, добавленный в предыдущем разде­ ле, заменяется выражением @await Component . InvokeAsync, которое обращается к компоненту представления. Листинг 25.15. Использование компонента представления в файле _ Layout. cshtml <!DOCTYPE html> <html> <head> <meta name= " v iewport" content= " width=device-width " /> <title>Citi es</title> <link asp- href- include=" /liЬ/bootstrap/dist/** /* .min. css " rel= " stylesheet " /> </head> <body class= " panel-body " > @await Component. InvokeAsync ("Time") <div>@RenderBody()</div> </body> </html> Запустив приложение, вы увидите баннер, отображающий время, когда содержи­ мое бьvю визуализировано. Подождите несколько секунд и перезагрузите страницу; вы заметите, что отображаемое время изменилось (рис. 25.6). New York USA Sa11 Jose USA London New York Sал Jose Pa ris France . ! Paris 1 ! l lВI ! 1 1 L--·----------·-··---··--t UK USA USA France Рис. 25.6 . Отображение времени в примере приложения 8.539.000 8 ,406.000 998 ,537 2,244 .000 Элемент cache применяется для окружения содержимого, которое должно быть добавлено в кеш. В листинге 25.16 элемент cache используется для добавления в кеш вывода из компонента представления.
786 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 25.16. Кеширование содержимого в файле _ Layout. cshtml <!DOCTYPE htrnl> <html> <head> <rneta name="viewport" content="width=device-width" /> <title>Cities</title> <link asp-href-include="/liЬ/bootstrap/dist/**/*.rnin.css" rel="stylesheet" /> </head> <body c l ass="panel-body"> <cache> @await Component. InvokeAsync ( "Time") </cache> <div> @RenderBody()</div> </body> </htrnl> Применение элемента cache без каких-либо атрибутов сообщает инфраструктуре MVC о том, что для удовлетворения всех будущих запросов необходимо повторно ис­ пользовать отмеченное содержимое. Теперь после запуска приложения содержимое, сгенерированное компонентом представления, кешируется, поэтому даже при пере­ загрузке страницы будет отображаться то же самое показание времени. Совет. Применяемый классом CacheTagHelper кеш основан на памяти, т.е. его емкость ограничивается доступным объемом ОЗУ. При нехватке пространства содержимое из кеша начинает удаляться, а в случае останова или перезапуска приложения все содер­ жимое кеша утрачивается. Установка времени истечения для кеша Атрибуты expires-* позволяют указывать время, когда истекает срок существова­ ния кешированного содержимого, выражаемое как абсолютное время, время относи­ тельно текущего времени или промежуток времени, в течение которого кешированное содержимое не запрашивается. В листинге 25.17 с помощью атрибута expires-after указывается, что содержимое должно кешироваться на протяжении 15 секунд. Листинг 25.17. Установка времени истечения для кеша в фaйлe _Layout.cshtml <!DOCTYPE html> <html> <head> <meta name = " v iewport" content="width=device-width" /> <title>Cities</title> <link asp-href-include="/liЬ/bootstrap/dist/**/*.min.css" rel="stylesheet "/> </head> <body class="panel-body"> <cache expires-after="@TimeSpan.FromSeconds(15)"> @await Componen t .InvokeAsync("Tirne") </cache> <d i v>@RenderBody()</div> </body> </html>
Глава 25. Использование друг их встроенны х дескрипторных вспомогательных классов 787 Запустив приложение, вы заметите, что срок существования кешированных дан­ ных истекает спустя 15 секунд, после чего перезагрузка страницы приводит к вызову компонента предс тавления и кешированию нового содержимого на протяжении сле­ дующих 15 секунд. Установка фиксированного момента истечения Посредством атрибута e xp i r es - o n, принимающего значение Da t e Tirne , можно указывать фиксированный момент времени, при наступлении которого истечет срок существования кешированного содержимого (листинг 25.18). Листинг 25.18. Указание фиксированного момента истечения в файле_Layout. cshtml < ! DOCTYPE html> <html> <head> <meta name="vi ewport " content =" width=devi ce-width" /> <t i t l e>Cities</title> <li nk asp-href-include="/liЬ/bootstrap/dist/**/* .min . css" rel ="stylesheet" /> </head> <body class= " p a nel-body" > <cache expires-on="@DateTime.Parse("2100-0l-01")"> @awa i t Cornponent.InvokeAsync ("Tirne " ) </cache> <div>@RenderBody() </di v> </body> </html > Здесь указано , что данные должны кешироваться до 2100 года. Это не особенно полезная стратегия кеширования, поскольку приложение наверняка будет перезапу ­ щено до того, как начнется следующее столетие, но иллюстрирует способ указания фиксированного момента в будущем вместо выражения времени истечения относи­ тельно момента, когда содержимое попадает в кеш. Установка периода истечения с момента последнего использования Атрибут expires - sl i ding применяется для указания промежутка времени, по прош е ствии которого срок существования содержимого истекает, если оно не извле­ калось из кеша. В листинге 25.19 задан скользящий период истечения, составляю­ щий 1О се1<унд . Листинг 25. 19. Указание периода истечения с момента последнего использования кешированного содержимого в файле _ Layout. cshtml < ! DOCTYPE html > <html> <head> <meta name= "viewpor t " content=" width=device-widt h" / > <titl e>Cities</title> <link asp -href -in c lude= " / liЬ/b oot s trap/dist/* */* .min. css " rel="stylesheet" /> </he ad> <body cla s s= " pane l-body" > <cache expires-sliding="@TimeSpan.FromSeconds(lO)">
788 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC @await Component .InvokeAsync("Time") </cache> <div>@RenderBody()</div> </body> </html> Запустив приложение и периодически перезагружая страницу, вы сможете уви ­ деть эффект от наличия атрибута express - sliding. До тех пор, пока вы перезагру­ жаете страницу в рамках 1О-секундного интервала, будет применяться кешированное содержимое. В случае ожидания свыше 1О секунд перед перезагруз1юй страницы ке­ шированное содержимое отбрасывается, с помощью компонента представления гене­ рируется новое содержимое, после чего процесс начинается заново. Использование вариаций кеша По умолчанию все запросы получают одно и то же 1<ешированное содержимое. Класс Cache TagHelpe r способен поддерживать разные версии кешированного со­ держимого и применять их для удовлетворения различных типов НТГР-запросов, указываемых с использованием одного из атрибутов, имена которых начинаются с vary- by . В листинге 25.20 демонстрируется применение атрибута vary- by - route для создания вариаций кеша на основе значения action, предоставляемого системой маршрутизации. Листинг 25.20. Создание вариации кеша в файле_Layout. cshtml <!DOCTYPE html> <html> <head> <meta name= " viewport" content="width=devi ce - width " /> <title>Cities</title> <l ink asp-href-include="/lib/bootstrap/dist/**/*.rnin.css" rel="stylesheet"/> </head> <body c lass="panel-body " > <cache expires-sliding="@TimeSpan.FromSeconds(lO)" vary-by -route="action"> @await Component . InvokeAsync("Time") </cache> <d iv>@RenderBody()</div> </body> </html> Если вы запустите приложение и воспользуетесь двумя вкладками или окнами браузера для запроса URL вида /Horne/ Index и /Horne/Create, то заметите, что каждая вкладка или окно получает собственное кешированное содержимое со своим сроком истечения, т.к. каждый запрос генерирует разное значение маршрутизации action. Класс CacheTagHelper поддерживает ряд атрибутов, которые определяют разнообразные вариации, включая кеширование содержимого для индивидуальных пользователей. Существует также атрибут vary-by, который позволяет определять произвольные вариации кеша с применением любого значения данных. В листинге 25.21 воссозда­ ется эффект от атрибута vary - by - route за счет указания значения, получаемого прямо из данных маршрута.
Глава 25. Использование других встроенных дес к рипторны х вспо мо гат е льны х к лассов 789 Листин г 25 .21 . Указание специальной вариации кеша в файле_Layout. cshtml <!DOC TYP E html> <html> <head> <meta name= " viewport " content = " width=device- width" /> <title>C i ties</title > <link asp-href-incl ude= " /li b/bootstrap/dist / ** / *.min.css" r el="st yl esheet"/ > </head> <body class=" panel -body " > <c ache expires-sliding="@TimeSpan.FromSeconds(lO)" vary-by= 11 @ViewContext.RouteData.Values[ 11 action 11 ] 11 > @await Component . I nvokeAsync( " Time " ) </cac h e> <di v>@RenderBody( )</div> </body> </html> Атрибут vary- by может использоваться для создания более сложных вариаций кеша, хотя его применение требует осторожности, поскольку довольно легко осла­ бить внимание и создать в итоге вариации, которые настолько специфичны, что со­ держимое в кеше никогда не будет задействовано повторно до истечения срока его существования . И спользо ва ние URL , относительных к приложению Последний встроенный дескрипторный вспомогательный класс, Ur lReso l ut ion TagHelper , прим е ня е тся для обеспечения поддержки URL, относительных к при­ ложению, которы е представляют собой URL, снабженные префиксом в виде тильды (символа - ). В листинге 25.22 показано изменение элемента lin k внутри разделяе­ мой компоновки, чтобы он использовал явно определенный URL вместо URL, сгенери­ ров анного из систе мы маршрути з ации с помощью дескрипторных вспомогательных классов . Листинг 25 .22 . Применение явного URL в фaйлe _Layout.cshtml <!DOCTYPE h t ml> <html> <head> <meta name= "viewport " c ontent =" widt h=devi ce- width" /> <title>Cit i es</t itl e> <link href="/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> </head> <body cl ass= "panel- body"> <cache expires - sliding= " @Time Span.FromSe cond s( l O) " vary-by= " @ViewContext .RouteDat a .Values ["act ion"] "> @await Component . InvokeAs y nc( " Time " ) </cach e> <div>@Ren d erBody( ) </di v> </body> </htm l >
790 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Явные URL прекрасно подходят при условии, что вы осознаете необходимость их обновления при изменении схемы URL приложения. И для многих приложений это единственный ф актор, который след;ует учитыв ать. Однако есть приложения, которые буд;ут предназначены для разделя емой среды, где единственный сервер поддерживает множество приложений, различ аемых путем доб авл е ния префикса к URL. В листинге 25.23 приведена измен енная конфигурация приложения, так что конвейер запросов настраивается для обработки запросов с пре­ фиксом mvcpp , эмулируя разделяемую среду. Листинг 25.23. Добавление префикса URL в файле Startup. cs usi ng Mi c r os oft.AspNetCore .Builder; using Mic r osoft. E x t ensions . Depend ency inj ect i on ; using Cities . Mode l s ; namespac e Citi es { puЫic clas s St a rtup puЬlic void Co n f i gureS ervice s(I S erviceCol l ection services) services.AddSingl et on<IReposit ory, MemoryReposi t ory>(); services . AddMvc() ; puЫic void Confi gure(IApp l icat i onBuilde r арр) { арр. Мар ( 11 /mvcapp 11 , appBuilder => { appBuilder.UseStatusCodePages(); appBui1der.UseDeve1operExceptionPage(); appBuilder.UseStaticFiles(); appBuilder.UseМvcWithDefau1tRoute(); }); Метод М а р () позволя ет настраивать несколько конвейеров запросов с р аз ны ми префиксами. Это не особенно полезная возможность при повседневной р азработке приложений MVC, потому что создавать префиксы URL внутри приложения MVC мож­ но с использованием системы маршрутизации . Но для настоящей главы это удобное средство, т.к. оно означа ет. что каждый URL запрашивается клиентами , включая за­ просы к статическому содержимому . Возникшую проблему можно заметить, запустив приложение и запросив URL вида / mvcapp, который теперь является стандартным URL для прилож ения и нацел ен на действие Index контроллера Ноте . Теперь , когда все URL должны начинаться с / mvcapp , явный URL для таблицы стилей в элем енте link не рабо т ает, по этому со­ держимое в приложении не может быть стил изовано (рис . 25.7). Проблему можно бьuю бы устранить, обновив явный URL для включения префик­ са, но это не всегда удается, т.к. префикс может измениться при развертывании или не быть известным на этапе разработки. Более эффективное решение пред;усматрива­ ет применение URL, относительных к приложению, когда путь к статиче скому содер­ жимому выражается относительно любого префикса , который мож ет быть сконфигу­ рирован (листинг 25.24) .
Глава 25 . Использование други х встроенны х дескрипторны х вспомогательны х к лассов 791 +- -> С ,- [] localhost:S1036/ mvcapp Re11derecl at 16:59:05 :'llaшe Couut1·y Popt1latio11 London UК 8.5 39.000 Ne" ' York USA 8.406.000 Sall Jo$e U SA P aris Create Fra11ce 998.53 7 2.244 .000 Рис. 25. 7. Эффект от явно определенного URL Листинг 25.24 . И спользование URL, относительного к приложению, в файле _Layout.cshtml <!DOCTYPE html> <html> <head> <meta name= "viewpor t " content= " wi dth=devi ce- width" /> <title>Cities</ti tle> "{;{: - <link href="~/lib/Ьootstrap/dist/css/Ьootstrap.min . css" rel="stylesheet" /> </head> <body cl ass="panel-body"> <cache expires - sliding=" @T im eSpan .FromSeconds(lO) " vary-by=" @ViewContext .Rout eDat a .Values["acti on"J "> @await Component . Inv o keAsync( "T ime " ) </cache> <div>@RenderBody()</div> </body> </html> Символ тильды обн аруживается классом Ur l ResolutionTagHe l per , который зам еняет его путем , требующимся для достижения содержимого папки wwwroo t. Запустив прилож ение , вы увидите, что содержимое стилизовано , а в результате про­ смотра НТМL-разметки, отправляемой браузеру , выяснится, что элемент l i nk содер­ жит URL . который включает префикс mvcapp : <link href= "/mvcapp /liЬ/bootstrap/dist/c ss /bootstrap . mi n . cs s" rel=" s t yle s he e t " /> Вспомогательный класс UrlRe sol u t i o n Tag ищет в URL широкий диапазон эле ­ ментов, как описано в табл. 25.9 . Со вет. Если вы применяете другой встроенный дескрипторный вспомогательный класс для генерации URL из систе м ы маршрутизации, тогда генерируемая им НТМL-размет ка будет автоматически включать любой необходимый префикс, который получается из свойства контекста HttpRequest . PathBase , а его значение предоставляется сервером, разме­ щающим прило жение.
792 Ч асть 11 . П одробные сведения о б инфрас тр укту р е ASP.NET Core MVC Таблица 25.9 . Элементы и атрибуты, трансформируемые классом UrlResolutionTagHelper Элемент Атрибуты а href applet archive area h ref audio src base href Ьlockquote cite button formaction del cite emЬed src form action html manifest iframe src img src, srcset input src, formaction ins cite link href menuitem icon object archive,data q cite script src source src, srcset track src v ideo src, poster Резюме В настоящей главе рассматривались встроенные дескрипторные вспомогательные классы, которые не связаны с НТМL-формами . Эти дескрипторные вспомогательные классы помогают управлять доступом к файлам JavaScript и таблицам стилей CSS, создавая URL для якорных элементов, выполняя аннулирование кеша для изображе ­ ний, кешируя данные и трансформируя URL , относительные к приложению . В сле ­ дующей главе будет представлена система привязки моделей, которая используется для обработки данных в НТТР-запросах, так что их можно легко потреблять внутри приложения МVС .
ГЛАВА 26 Привязка моделей привязка моделей - это процесс создания объектов .N ET с использованием дан­ ных из НТГР-запроса для снабжения методов действий аргументами, в которых они нуждаются. В настоящей главе будет описана работа системы привя зки моделей, показано , как она привязывает просты е типы, сложные типы и коллекции. а также продемонстрировано, каким образом взять на себя контроль над проце ссом, чтобы указывать часть запроса, которая предоставляет значения данных, требующиеся ме­ тодам действий. В табл. 26.1 прив едена свод1\а, позволяющая пом естить привязку моделей в контекст. Таблица 26. 1. Помещение привязки моделей в контекст Вопрос Что это такое? Чем она полезна? Как она используются? Существуют ли какие- то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменилась ли она по сравнению с версией MVC 5? Ответ Привязка моделей представляет собой процесс создания объектов , которые требуются методам действий в качестве аргументов, с приме­ нением значени й данных, получаемых из НТТР-запроса Привязка моделей позволяет методам действий объявлять параметры, используя типы С#, и автоматически получать данные из запроса без не­ обходимости в инспектировании, разборе и обработке данных напрямую В своей простейшей форме методы действий объявляют параметры, имена которы х применяются дпя извлечения значений данных из НТТР-запроса. Часть запроса, используемая для получения данных, может быть сконфигу­ рирована за счет применения атрибутов к параметрам методов действий Основная ловушка - получение данных из неправильной части за­ проса . Способ поиска данны х внутри запросов объясняется в разделе " Понятие привязки моделей" далее в главе, а места для по и ска могут быть указаны явно с использованием атрибутов, которые описаны поз­ же в разделе " Указание источника данных привязки моделей" Методы де йствий вообще не обязаны объявлять параметры , и могут получать данные прямо из НТТР-запроса с применением объектов кон­ текста, которые были описаны в главе 17 . Однако результа том будет более слож ный код, который труднее читать и сопрово ждать В инфраструктуре ASP.NET Core MVC средство привязки моделей было переписано, но работает так же, как в предшествующи х версиях. Самое заметное изменение состоит в том, что атрибут Bind больше не может ис пользоваться для исключения свойств модели из процесса привязки, что теперь делается с помощью атрибута BindNever. За подробными сведениями обращайтесь в раздел "И збирательная при­ вяз ка свойств" далее в главе
794 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC В табл. 26.2 приведена сводка для этой главы. Таблица 26.2 . Сводка по главе Задача Привязка простого типа или коллекции Привязка сложного типа Решение Добавьте параметр к методу действия Удостоверьтесь , что НТМL-разметка, генерируемая представлением, хорошо структурирована Листин г 26 .1-26.11 , 26 .24 -26.30 26 .12-26.20 Избирательная привяз ка Укажите имена значений данных с применени- 26.21-26.23 свойств ем атрибута Bind либо используйте атрибут BindNever для исключения свойств модели из процесса привязки Указание источника Примените к аргументу метода действия или к 26.31-26.39 значения привязки данны х свойству модели атрибут, который идентифициру­ ет, откуда дол жно поступать значение привязки Подготовка проекта для примера Созда йте новый про ект ти па Empty (Пустой) по имени MvcModels с использова­ ни ем шаблона ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пак еты NuG et в ра здел dependencies файла project . j son и настройте инструментарий Razor в разделе tools, как показано в листинrе 26. 1 . Разд елы , которые не нужны для данной главы, понадобится удалить. Листинг 26.1. Добавление пакетов в файле project. j son " dependencies ": { " Microsoft . NETCore . App ": "version": "1.0.О", " type" : "p latform " }1 }1 " Microsoft . AspNetCore . Diagnostics ": " 1 . 0 . 0 ", " Microsoft . AspNetCore . Server.IISintegration ": "1.0 .0", " Microsoft.AspNetCore . Se rver.Kest re l": " 1 . 0 . О ", " Microsoft.Extensions . Logging . Console ": " 1 . 0 . 0 ", "Microsoft.AspNetCore .Mvc": 11 1.0.0 11 , "Microsoft.AspNetCore.StaticFiles": 11 1.0 .0 11 , "Microsoft.AspNetCore.Razor.Tools": "vers ion": "1. О. O-preview2-final", "type": "build" " tools ": "Microsoft.AspNetCore.Razor . Tools": "1.0 .0-preview2-final", " Microsoft . AspNetCore . Server.IISintegration.Tools ": " 1 . 0 . 0 - preview2 - final " }1
" frameworks ": { " netcoreappl.0 ": Глава 26 . Привяз ка моделей 795 " imports ": [ " dotnet5 . 6 ", "portaЬle-net4 5+win8 "] }' "buildOptions": " emitEntryPoint ": true, " preserveCompilationContext ": true }' "runtimeOptions": { " configProperties ": { " System . GC .Server ": true} Создание модели и хранилища Создайте папку Models и добавьте в нее файл класса по им ени Person . cs с опре­ делением классов и перечисления из листинга 26.2 . Листинг 26.2 . Содержимое файла Person. cs из папки Models using System ; namespace MvcMode l s . Models puЬlic class Person { puЫic int Personld { get ; set ; } puЫic string FirstName { get ; set; puЫic string LastName { get ; set; } puЫic DateTime BirthDate { get ; set ; } puЫic Address HomeAddress { get ; set ; } puЬlic bool IsApproved { get ; set ; } puЫic Role Role { get; set; } puЫic class Address { puЬlic string Linel { get ; set; } puЫic string Line2 { get ; set ; } puЫic string Ci ty { get; set ; } puЬlic string PostalCode { get ; set ; puЫic string Country { get; set ; } puЫic enum Role { Admin , User , Guest Добавьте в папку Models файл класса по имени Reposi tory . cs и поместите в него определения интерфейса и класса реализации, приведенные в листинге 26.3 .
796 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 26.3 . Содержимое файла Reposi tory. cs из папки Models u s ing System . Collect ions . Generic ; namespace MvcModels . Mode l s { puЬlic i n t erface IRepository { IEnumeraЫe<Person> People { get ; Person this[int id] { get ; set; ) puЫic c l ass MemoryRepository : IRepos itory private Dictionary<int , Person> people new Dictionary<int, Person> { [1] = new Person {Personid = 1, Fir stName "ВоЬ", LastName = " Smi th", Role = Role .Admin) , }; [2] = new Person {Personid = 2 , First Name "Anne", LastName = " Do uglas ", Role = Role . User) , [3] = new Pers on {Personi d = З , Fi rstName " Joe", LastName = "АЫе", Role = Role.User}, [4] = new Person {Personid = 4, FirstName "Mary ", LastName = " Peters ", Ro l e = Role . Guest} puЬlic IEnumeraЫ e <Person> Pe ople => peopl e .V alues ; puЬlic Person t his [int i d] { get { return peopl e .ContainsKey(id) ? peopl e[id] set { people[id] value ; nul l; В интерф ейсе IReposi tory опред елено свойство Peop l e для извлечения всех объ­ ектов модели и индексатор, который позволяет извлекать или сохранять отдельные объекты Person. Кл асс MemoryReposi t ory реализует этот интерфейс и прим еняет словарь со стандартным содержимым . Реализация хранилища не поддерживает пос­ тоянство, так что после останова или перезапуска состояние приложения во з вр аща­ ется к стандартному содержимому . Создание контроллера и представления Создайте папку Cont ro11еr s , добавьте в нее файл класса по имени HomeCon troller . cs и опр еделите контроллер, код которого показан в листинге 26.4 . Контроллер полагается на внедрение зависимостей при получении хр анилища, 1юто­ рое он использует в методе Index () для выбор а одиночного объекта Person с приме ­ нением значения его свойства Personi d . Листинг 26.4. Содержимое файла HomeController. cs из папки Controllers using Microsoft . AspNetCo r e . Mvc ; u sing MvcModels . Mode l s ;
namespace MvcModels.Controllers { puЫic class HomeController : Cont r ol ler private IReposito r y reposito ry; puЫic HomeController( I Repository repo) { repository = repo ; Глава 26 . Привязка моделей 797 puЫic ViewRes ult Index( int id) => View(repository[id]); Чтобы снабдить метод действия представлением, создайте папку Vi ews/Home и добавьте в нее файл представления Razor по имени Index. cshtml с разметкой из листинга 26.5, которая отображает ряд свойств объекта модели в таблице. Листинг 26.5 . Содержимое файла Index. cshtml из папки Views/Home @model Person @{ Layout = " Layout"; ) <div class= " bg - primary panel -b ody "><h 2>Person</h2></div> <tаЫе class= " taЫe taЬle - condensed taЬle-bordered taЫe - striped " > <tr><th>Personid : </th><td>@Model . P ersoni d</td></tr> <tr><th>First Name : </th><td>@Model . FirstName</td></tr> <tr><th>Last Name : </th><td>@Model . LastName</td></tr> <tr><th>Role : </th><td>@Mode l.Role</td></tr> </tаЫе> Представление Index. cshtm l рассчитывает на разделяемую компоновку. Создайте папку Views/Shared и поместите в нее файл по имени Layout . cshtml, содержимое которого приведено в листинге 26.6. Листинг 26.6 . Содержимое файла _Layout.cshtml из папки Views/Shared < ! DOCTYPE html > <html> <head> <meta name="viewport" content= " width= device-w idth " /> <title>@ViewBag . Title</title> <link asp - href -i nclu de= " /liЬ/bootstr ap /dis t /**/* . min . css " rel= "s tylesheet " /> @Rende r Section( "scr ipts ", fa ls e) </head> <body class= " panel -b od y" > @RenderBody () </body> </html> Компоновка включает элемент link для таблицы стилей Bootstrap и визуализи­ рует содержимое представления. Имеется также дополнительный раздел scripts, который будет использоваться позже в главе. Чтобы упростить представления , при­ меняемые в этой главе, добавьте в файл_Viewimports. cshtml из папки Views про­ странство имен, содержащее классы модели (листинг 26.7).
798 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 26. 7 . Импортирование пространства имен в файле _View!mports . cshtml @using MvcModels . Models @addTagHelper * , Microsoft . AspNetCore . Mvc .TagHelpers Представления в примере опираются на СSS-пакет Bootstrap. Создайте в корневой папке проекта файл bower . j son с применением шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте пакет Bootstrap в раздел dependencies (листинг 26.8). Листинг 26.8. Добавление пакета Bootstrap в файле bower. j son "name ": "asp.net", " private ": true , " dependencies ": { "bootstrap": 11 3.3.6 11 Конфигурирование приложения В листинге 26.9 показан код класса Startup, который конфигурирует средства, предоставляе мые пакетами NuGet, и настраива ет службу хранилища. Листинг 26.9. Содержимое файла Startup. cs using Microsoft.AspNetCore.Builder ; using Microsoft.Extensions.Dependencyinjection; using MvcМodels . Models; namespace MvcModels { puЫic class Startup puЬlic void ConfigureServices(IServiceCollection services) services.AddSingleton<IRepository, MemoryRepository>(); services.AddМvc(); puЫic void Configure(IApplicationBui l der арр) { app.UseStatusCodePages(); app . UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); Запустите прилож ение и запросите URLвида /Horne/Index/1; результат пр едстав­ лен на рис. 26.1 . (В данный момент запрос стандартного URL вызывает ошибку . )
J lcx:alh ost:S186 1/Home/l r Х -- -- '== =::: Глава 26. Привязка моделей 799 ~ -+ С [] localh o st:51861/Home/lndex/1 '{::s , - Personl d: Fi rst Name: Last Name: Role : ВоЬ Smith Admi n _______ __, Рис. 26.1 . Выполнение примера приложения Понятие привязки моделей Привязка моделей - элегантный мост между НТГР-запросом и методами действий С#. Большинство приложений МVС Framework в той или иной степени полагаются на привязку моделей, в том числе простой пример приложения, созданный в предыду­ щем разделе. Привязка моделей использовалась при тестировании примера приложения из пре­ дыдущего раздела. Запрошенный URL содержал значение свойства Pe rsonid объекта Perso n, подлежащего отображению: /Home/ Index/ 1 Инфраструктура MVC транслировала эту часть URL и применила ее в качест­ ве аргумента, когда вызывала метод Index () контроллера Home, чтобы обслужить запрос: puЫic ViewR esult I ndex(int id ) => View( rep o sitory[id ] ); Чтобы иметь возможность вызьmать метод Inde x () , инфраструктуре МVС необхо­ димо значение для аргумента id. Снабжение этим значением входит в число обязан­ ностей системы привязки моделей, отвечающей за предоставление значений данных, которые могут использоваться при вызове методов действий. Система привязки моделей опирается на связыватели моделей, которые являются компонентами, ответственными за предоставление значений данных из одной части запроса или приложения. Стандартные связьmатели моделей ищут значения данных в следующих трех местах: • отправленные данные формы; • переменные маршрутизации; • строки запросов.
800 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Каждый источник данных инспектируется по порядку до тех пор , пока не будет обнаружено значение для аргумента. В рассматриваемом примере данные формы отсутствуют, поэтому значение в них не может быть найдено. Но в конфигурации приложения, приведенной в листинге 26.9 , предусмотрен сегм е нт маршрутизации по имени id, который позволит системе привязки моделей снабдить инфраструктуру МVС значением, подлежащим применению при вызове метода I n dex () . После того, как найдено подходящее значение , поиск останавливается, т.е. строка запроса небу­ дет просматриваться на предмет наличия в ней значения данных. Совет. В разделе "Указание источника данных привязки моделей" далее в главе объясняет­ ся, как можно указать источник данных привязки моделей с использованием атрибутов . Это позволяет установить, что значение данных должно получаться, скажем, из строки запроса, даже если подходящее значение также есть в данных формы или переменных маршрутизации. Знание порядка поиска значений данных важно из-за того, что запрос может со­ держать несколько значений , например: /Home/Index/З?id=l Система маршрутизации обработает запрос и сопоставит сегмент id в шаблоне URL со значением 3, но строка запроса содержит значение i d , равное 1. Поскольку данные маршрутизации просматриваются раньше строки запроса, метод действия Index () получит значение 3, а значение из строки запроса будет проигнорировано. С другой стороны , в случае запроса URL, который не имеет сегмента id, будет ис­ следоваться строка запроса , т.е. URL вроде приведенного ниже также позволит сис ­ теме привязки моделей предоставить инфраструктуре МVС значение аргумента id, чтобы она смогла вызывать метод Index ( ) : / Hom e /Index ?id=l Результаты запросов обоих URL показаны на рис . 26.2 . Pe rsonld: 3 Pe rsonld: First Name: Joe Fi rst Na me: ВоЬ LastName: АЫе Last Name : Smith Role: Admin Рис. 26.2. Эффект от упорядочивания источников данных привязки моделей
Глава 26 . П ривязка моделей 801 Стандартные значения привязки Привязка моделей является негарантированным средством, т.е. МVС будет при­ менять его в попытке получить значения, необходимые для вызова метода действия, но все равно вызовет метод, даже если значения данных предоставить не удалось. Это может приводить к непредсказуемому поведению. Например, запрос URL вида /Home/Index генерирует исключение (рис. 26.3) . D lnterna! Server Error Х г~·-·---·-------·--·--·--·----·-----·--~·---- 1-·~-----~-l-~,:~~-'~~;~~~~~~:1:;;~~,::_,.:,===·==:=.:==-=- *l -.: _ 1 An Linhaпdl ed exception оссштеd \1Vh il e processing the 1 Irequest. · 1 NL1 llRefe1·enceExceptioп : Obj ect reference not se t to an inst a1се of ап o!Jject. ~ ! Asp. _Vie,vs_Home_111dex_csl1tm l .< ExecuteAsyno d_17.MoveNoxtO 1 OL1ery Cookies Headers 1 1 ~~~e~9l>~e~~~bl~~~:~~~1. a~~ Рис. 26.3. Ошибка обраб отки свойства м одел и Исключение было инициировано не системой привязки моделей. Взамен оно про­ изошло при обработке представления Index, выбранного методом действия Index () . Чтобы вызвать метод Index (), инфраструктура MVC должна обеспечить значение для аргумента id, а потому она предлагает каждому связывателю модели проинспек­ тировать свою часть запроса и предоставить значение. В примере отсутствуют данные формы, не указано значение для сегмента мар­ шрутизации id и нет строки запроса в URL, так что система привязки моделей не в состоянии предоставить значение данных. Чтобы вызвать метод Index () , инф­ раструктура MVC обязана предоставить какое-нибудь значение для аргумента id, поэтому она использует стандартное значение и надеется на лучшее. Стандартным значением для аргументов int является О, что и приводит к генерации исключения. В методе Index () значение аргумента id применяется при извлечении объекта мо­ дели из хранилища: puЫic ViewResult Index(int id) => View(repository[id] ); Когда MVC использует стандартное значение, метод действия пытается извлечь объект модели с id, равным О. Такой объект не существует, из-за чего хранилище воз­ вращает значение null, которое затем передается методу View () контроллера, что­ бы указать данные модели представления для представления Index. cshtml. Когда выражение Razor в файле Index. cshtml пытается обратиться к свойствам объекта модели представления, возникает исключение NullReferenceException, как было проиллюстрировано на рис . 26.3 .
802 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Это означает, что методы действий должны быть написаны так, чтобы справиться со стандартными знач ениями , предоставляемыми сист е мой привя з ки м одел ей, что может быть сделано несколькими способами. Можно добавить стандартные значе­ ния в шаблоны URL маршрутов (как было описано в главе 15), присвоить стандар­ тные значения параметрам методов действий или обесп еч ить , чтобы методы де йс­ твий не передавали недопустимые значения данных в качестве части своих ответов . Предпочтительный подход будет зависеть от того , что предприним ает метод действия; в листинге 26.10 избран последний подход, который предусматривает изм енение ме­ тода действия так, что он всегда передает методу View ( ) объект Person, даже если аргумент i d не соответствует какому-то объекту в модели данных. Листинг 26.1 О. Защита от стандартных значений привязки модели в файле HomeController. cs using Mic r o s o f t.AspNetCore. Mvc ; usi ng MvcModel s .Models; using System.Linq; names p ace MvcModels . Contro l lers puЫ ic class HomeControlle r : Cont r o l ler private I Repos itory repository ; puЫic HomeControl l er( I Reposi t ory repo) { repository = repo ; puЫic ViewResult Index(int id) => View(repository[id] ?? repository.People.First()); В методе действия с при м енением LINQ и операции объединения с null возвра­ щается первый объект из хранилища, если значение параметра id не прив ело к из­ влечению объекта. Привязка простых типов Когда доступно подходящее значение, оно преобразуется в значение типа С#, ко­ торое может использоваться при вызове метода действия. Простые типы - это зна­ чения, порожденны е от одного элемента данных в запросе, который может быть ра­ зобран из строки. В их состав входят числовые значения , булевские знач ения , даты и, разумеется, значения strin g . Аргумент i d метода действия Index ( ) имеет тип int, так что процесс привязки модел ей снабжает инфраструктуру MVC значением за счет разбора пер еменной сег­ мента id в значение int. Если значение из запроса не может быть преобразовано (например, для пара­ метра, требующего значение i n t, указано значение a p p le), тогда процесс привязки моделей окажется неспособным предоставить значение приложению, и будет приме­ няться стандартное значение. В итоге возникает проблема, т. к. метод действия будет получать стандартное зна­ чение О в двух ситуациях. Первая из них - когда запрос содержит значение, которое не может быть преобразовано в тип аргумента , как в URL вида /Home/Index/Apple. Вторая ситуация - когда запрос содержит значение, поддающее ся преобразованию , но равное О, как в URL вида /Home/Index / 0.
Глава 26. Привязка моделей 803 Большинству приложений нужна возможность различения таких ситуаций, и лег­ че всего это делается с использованием для аргумента метода действия типа, допус­ кающего null (листинг 26.11). Листинг 26.11. Применение типа, допускающего null, в файле HomeController. cs using Microsoft.AspNetCore.Mvc; using MvcModels.Models; namespace MvcModels.Controllers puЫic class HomeController : Controller private IRepository repository; puЫic HomeController(IRepository repo) { repository = repo; puЫic IActionResult Index(int? id) { Person person; if (id.HasValue && (person = repository[id.Value]) != null) { return View(person); else { return NotFound(); Стандартным значением для типов, допускающих null, является null. Оно поз­ воляет проводить различие между запросами, не содержащими значение, которое может быть преобразовано в int, и запросами со значением, поддающимся преобра­ зованию в int, но равным О. Реализация метода Index () в этом примере использует метод NotFound () для возвращения ошибки 404, если аргумент типа, допускающего null, не имеет значения или если его значение не соответствует какому-то объекту в модели. Такой подход более надежен, чем просто полагаться на то, что первый объект в модели окажется подходящим, как делалось в предыдущем разделе. ПривS1зка сложных типов Когда параметр метода действия имеет сложный тип (другими словами, любой тип, который не может быть разобран из одиночного строкового значения), то процесс привязки моделей применяет рефлексию для получения набора открытых свойств це­ левого типа и выполняет процесс привязки в отношении каждого из них по очереди. Чтобы посмотреть, как это работает, добавьте в контроллер Horne два метода действия (листинг 26.12). Листинг 26.12. Добавление методов действий в файле HomeController. cs using Microsoft.AspNetCore.Mvc; using MvcModels.Models; namespace MvcModels.Controllers puЫic class HorneController : Controller private IRepository repository;
804 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic HomeController(IRepository repo ) { repository = rep o ; puЫic IAc tionResult Index(int? id) { Person person ; if (id . HasValue && (person = repository[id . Value]) ! = null) { return View(person) ; else { return NotFound() ; puЬlic ViewResul t Create () => View (new Person ()) ; [HttpPost] puЫic ViewResult Create (Person model) => View("Index" , model); В ерсия метода Create () без пар аметров создает новый объект Person и п ере­ дает его методу View (), который выбирает стандартное представление, ассоцииро­ ванное с действием. Добавьте в папку Views/Home файл пр едставления по имени Create . cshtml и по м естите в н его разметку, прив еденную в листинге 26.13. Листинг 26.13. Содержимое файла Create.cshtml из п апки Views/Horne @model Person @{ ViewBag . Title = "Create Person "; Layout = " Layout "; <form asp - action="Create " method= " post " > <div class= " form - group " > <labe l asp - for="Person i d " ></label> <input asp - for= " Personid " class= " form - contro l" /> </div> <div class= " form - group " > < label asp - for= " FirstName " ></ label > <input asp-for= " FirstName " class= " form-control" /> </div> <div cl ass="form- group"> <label asp - for= "LastName " ></ label > <input asp-for= "LastName " class= " form - control " /> </div> <div class= " form-group " > <label asp - for= " Role " ></label> <select asp - for= " Role " class= " form - control " asp - items= " @new SelectList(Enum . GetNames(typeof(Role)))"> </select> </div> <button type="submit" c lass="Ьtn Ьtn - primary " >S ubmi t</button> </form>
Глава 26. Привязка моделей 805 Представление содержит форму, позволяющую предоставлять значения для ряда свойств объекта Person, элемент form которой отправляет данные обратно версии метода Create () контроллера Ноте, декорированной атрибутом HttpPost. Метод действия, который получает данные формы, использует для их отображе­ ния представление /Views/Home/Index . cshtml. Чтобы посмотреть на все это в ра­ боте, запустите приложение, перейдите к URL вида /Home/Create, заполните форму и щелкните на кнопке Submit (Отправить); результат показан на рис. 26.4 . 1~ Cr•at• Pe~on Х 1 r- ·- ------------ t-+ - -+ _ С . : [J~call1ost:~~~:.~~:~~~~ -::r-=- Personld localhost:S186 1 /Нomo/C Х 100 С С:! tocalho st :51861/H o me/Cгeate <{;:s _ FirstNam e Peter LastName Personld: 100 James First Name: Peter 1 ~~'~-·--·----.. ~ ~:::Nome l--~~-~~----· '-------- ---_] James Guest Рис . 26.4. Применение методов действий Crea te () После отправки данных формы серверу процесс привязки моделей обнаруживает, что метод действия требует сложного типа - объекта Person. Класс Person исследу­ ется на предмет открытых свойств. Для каждого свойства простого типа связыватель модели пытается найти значение в запросе, как делалось в предыдущем примере. Таким образом, например, связыватель модели встречает свойство Personid и ищет значение Personid в тех же самых местах. где производился поиск значения id в предыдущем разделе. Поскольку данные формы содержат подходящее значение, настроенное с использованием дескрипторного вспомогательного класса asp - for в элементе i nput, именно это значение и будет применено . Если свойство требует другого сложного типа, тогда процесс повторяется для ново­ го типа. Сначала получается набор открытых свойств и связыватель пытается найти значения для всех этих свойств. Разница в том, что имена свойств являются вложен­ ными. Например, свойство HomeAddress класса Person имеет тип Address , как вы­ делено ниже:
806 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC using System ; namespace MvcModels . Models puЫic class Person { puЫic int Personid { get ; set ; } puЫic string FirstName { get; set; puЬlic string LastName { get ; set; } puЫic DateTime BirthDate { get; set ; ) puЫic Address HomeAddress { get; set; } puЫic bool IsApproved { get; set; ) puЫic Role Role { get; set; ) puЫic class Address { puЫic string Linel { get; set; } puЫic string Line2 { get; set; } puЫic string City { get; set; } puЫic string PostalCode { get; set; puЬlic string Country { get; set; } puЫic enum Role { Admin, User, Guest При поиске значения для свойства Linel связыватель модели ищет значение для HomeAddress. Linel, т.к. имя свойства в объекте модели комбинируется с именем свойства во вложенном типе модели. Создание легко привязываемой НТМL-разметки Использование префиксов означает, что представления должны включать инфор­ мацию. интересующую связыватель модели. Это легко делается с применением де­ скрипторных вспомогательных классов, которые автоматически добавляют требуе­ мые префиксы к трансформируемым элементам. В листинге 26.14 форма расширена, чтобы принимать данные адреса. Листинг 26. 14 . Обновление формы в файле Create . cshtml @model Person @{ ViewBag.Title = "Create Person"; Layout = " _Layout"; <form asp - action= " Create " method="post"> <div class= " form - group"> <label asp - for= " Personid"></label> <input asp -for= " Personid " class= "form - control " /> </div> <div class= " form - group " > <label asp - for= " FirstName " ></label> <input asp-for= " FirstName " class= " form - control " /> </div>
Глава 26. Привязка моделей 807 <div class= " form - group " > <label asp - for= " LastName " ></label> <input asp-for="LastName" cl ass=" form- control" /> </div> <div cl ass=" form-group"> <label asp - for= " Role " ></labe l > <select asp-for="Role" cl ass="form- cont rol" asp - items= " @new SelectList (Enum . GetNames(typeof(Role) )) " > </select> </div> <div class="form-group"> <label asp-for="HomeAddress.City"></label> <input asp-for="HomeAddress.City" class="form-control" /> </div> <div class="form-group"> <laЬel asp-for="HomeAddress.Country"></laЬel> <input asp-for="HomeAddress.Country" class="form-control" /> </div> <button type= " submit " class= "btn Ьtn - primary " >Submit</button> </form> Когда используется дескрипторный вспомогательный класс, вложенное имя свойс­ тва указывается с применением соглашений С# , так что внешние и вложенные имена свойств отделяются точками : Hom e Addre s s. Co unt r y . Запустив приложени е , запро­ сив URL вида /Home/Create и просмотрев НТМL-разметку. отправл е нную брауз еру . вы заметите , что для некоторых атрибутов используется другое соглашение: <div class= " form - group " > <laЬel for="HomeAddress_City">City</label> <input class="form-control" type="text" id="HomeAddress City" name="HomeAddress. City" value="" /> - </div> <div cl ass=" form- group"> <label for="HomeAddress_Country">Country</laЬel> <input class="form-control" type="text" id="HomeAddress Country" name="HomeAddress. Country" value="" /> - </div> Атрибуты name элементов i nput следуют стилю С#, но в атрибутах fo r эл ементов label и атрибут ах id элементов input имена свойств отделяются с помощью сим во­ лов подчеркивания . Если вы предпочитаете определять НТМL- элементы без дескрип ­ торных вспо могательных классов, то должны обеспечить прим енение одной и той же схем ы именования. Как следствие , предпринимать какие-либо специальные действия, чтобы гаранти­ ровать создани е связывателем модели объекта Address для свой ства Ho meAddress не понад обится. Это мо ж но прод е монстрировать, и з м е нив представление Index . cshtml для отображения свойств HomeAddress . когд а они отправляются из фор мы (листинг 26. 15) . Листинг 26.15. Отображение свойств HomeAddress в файле Index. cshtml @model Person @{ Layout = " Layout "; <div class = " bg - primary panel - bod y " ><h2>Person</h2></div>
808 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC <tаЫе class= " taЫe taЫ e - co ndensed taЫe-bordered taЫe-striped"> <t r ><th>Personid : </th ><td>@Mo del.P erson id</ td ></tr> <t r> <th>First Name:</th>< t d>@Mo d e l.FirstName</td></tr> <tr><th>Last Name : </th ><td>@ Model.LastNa me</td> </t r> <t r ><th>Role : </ th><td>@Model . Role</td></ tr> <tr><th>City:</th><td>@Model.HomeAddress?.City</td></tr> <tr><th>Country:</th><td>@Model.HomeAddress?.Country</td></tr> </tаЫе> Запустив приложение и перейдя на URL вида / Home /Create , можно ввести значе­ ния для свойств Ci ty и Country , после чего отправить форму и убедиться, что зна­ чения были привязаны к объекту модели (рис . 26.5). 1 Creatt P~rson Х Е- ~ С D localhost :51861 /Hom e/Create ---- - ·---- ----~ --~-------_,__::.__ ___ _ Pe rsonld 100 f loc.Jiho:; t:5 1 861/Нomt-/C х FirstNam e +- ~ с i ёi"i~;~ih;;-s~s18.611н;;~;ё;;;;t; -".---·-···-········ --:& · =1 John Las tNam e Peters Personld: 100 Role Flrst Name: John Last Name: Peters Guest Role : Guest City City: Paris Paris Country: France ________ г_____ __J 1 Count ry Frar1ce iiN ·---------··-____J Рис. 26.5. Привязка свойств в сложных объектах Указание специальных префиксов Встречаются ситуации, когда генерируемая НТМL-разметка относится к одному типу объекта, но ее необходимо привязать к другому типу. Это означает, что префик­ сы, содержащиеся в представлении, не будут соответствовать структуре, которую ожидает связыватель модели, и данные не смогут быть правил ьно обработаны. Чтобы взглянуть на проблему, добавьте в папку Models файл по имени AddressS umm a r y . cs с определением класса, показанным в листинге 26. 16.
Глава 26. Привяз ка моделей 809 Листинг 26.16. Содержимое файла AddressSummary. cs из папки Models namespace MvcModels.Models { puЫic class AddressSurnmary { puЫic string City { get; set; puЫic string Country { get; set; Добавьте в контроллер Ноте новый метод действия, который использует класс AddressSummary (листинг 26.17) . Листинг 26.17 . Добавление метода действия в файле HomeController. cs @model AddressSummary @{ ViewBag . Ti t l e = " DisplaySummary "; Layout = "_Layout"; <div class= "bg-primary panel - body"><h2>Address</h2></div> <tаЫе class= "taЫe taЫe-condensed taЫe-bordered taЫe - striped " > <tr><th>City : </th><td>@Model . City</td></tr> <tr><th>Country : </th><td>@Model . Country</td></tr> </tаЫе> Новый метод действия называется DisplaySummary (). Он принимает параметр AddressSummary , который передается методу View (),так что параметр может быть отображен в стандартном представлении. Создайте в папке /Views/Home файл DisplaySшmnary . cshtml и поместите в него разметку, приведенную в листинге 26.18. Листинг 26.18. Содержимое файла DisplaySummary. cshtml из папки /Views/Home @model AddressSummary @{ ViewBag . Ti tle = "DisplaySummary "; Layout =" Layout"; <div class="bg-primary panel-body"><h2>Address</h2></div> < t аЫе class= " taЫe taЫe-condensed taЬle-bordered taЫe-striped"> <tr><th>City : </th><td>@Model .City</td></tr> <tr><th>Country : </th><td>@Model .Country</td></tr> </tаЫе> Представление отображает значения двух свойств, определенных в классе AddressSummary. Для иллюстрации проблемы с префиксами во время привязки мо­ делей разных типов модифицируйте элемент form в представлении Create. cshtml, чтобы он отправлял данные действию DisplaySummary (листинг 26. 19).
810 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 26.19. Изменение целевого действия формы в файле Create.cshtml @model Person @{ ViewBag . Title = " Create Person "; Layout = " Layout "; <form asp-action="DisplaySummary" method="post"> < ! -- Для краткости НТМL-элементы не показаны --> </form> Запустив прилож ение и перейдя на URL вида /Home/CreatePerson, можно взгля­ нуть на происходящее. После отправки формы значения, которые были введены для свойств City и Country, не отображаются в НТМL-разм етке , сгенерированной пред­ ставлением DisplaySummary. Проблема в том, что атрибуты name в форме имеют префикс HomeAddress, не являю щийся тем, который ищет связыватель модели при попытк е привяз ат ь тип AddressSummary . Чтобы исправить проблему, к параметру метода действия можно прим енить ат­ рибут Bind, в котором указывается префикс , подлежащий испо л ьзованию во время привязки модели (листинг 26.20). Листинг 26.20. Изменение префикса привязки модели в файле HomeController. cs using Microsoft . AspNetCore .Mv c; using MvcModels . Models ; namespace MvcModels . Controllers puЫic class HomeController : Controller private IRepository repository ; puЫic HomeController(IRepository repo) { repository = repo ; puЬlic IActionResult Index(int? id) { Person person; if (id .H asValue && (person = repository [i d . Value]) ! = null) { return View(person) ; else { return NotFound() ; puЫic ViewResult Create(} => View(new Person()) ; [HttpPost ] puЫic ViewResult Create(Person model) => View( "Index ", model) ; puЫic ViewResult DisplaySumma ry( [Bind(Prefix = nameof(Person.HomeAddress))] AddressSummary summary) => View ( summary) ;
Глава 26. Привязка моделей 811 Синтаксис неудобен, но результат приемлем. При заполнении свойств объек­ та AddressSurnmary связыватель модели будет искать в запросе значения данных HomeAddress. Ci ty и HomeAddress. Country. Запустив приложение и отправив форму еще раз, вы увидите, что значения, введенные в поля City и Country, теперь отображаются корректно (рис. 26.6). Решение может выглядеть слишком длинным для такой простой проблемы, но потребность в привязке разных видов объектов воз­ никает на удивление часто, и об этом приеме полезно знать. r Cte.:ite Pt(SOn х 1 ~ -+ С ·с;- locaih-;;~·-t-:5-'1 8=6=1 /=H=onie/Cr;;\;- 1--·---~----·----·-· ··-·-·~-·· -· --·-----·--'-"-------~---· Person ld 100 t Dtspl&ySummг.ry FirstName " ~•lh~"s""/Homo/D~pl•,Som~ <:, "l Pele rs John LastName Role City: _J Paris С~у Use r Country: France Paris Рис. 26.б. Привяз ка свойств другого объектного типа Избирательная привязка свойств Предположим, что свойство Country класса AddressSummary является критичес­ ки важным, и пользователь не должен иметь возможность указывать для него зна­ чения. Первое, что мы можем предпринять - не допустить, чтобы пользователь мог видеть или редактировать свойство , удостоверившись в отсутствии внутри представ­ лений приложения любых НТМL-элементов, которые ссылаются на данное свойство. Тем не менее, злоумышленник может просто отредактировать данные формы, посьJлаемые серверу, во время отправки формы и указать нужное ему значение для свойства Country. На самом деле связывателю модели необходимо сообщить о том, чтобы он не привязывал свойство Country к значению из запроса. Для этого можно сконфигурировать атрибут Bind на параметре метода действия, указав имена только тех свойств, которые должны привязываться (листинг 26.21) .
812 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 26.21. Указание свойств, подлежащих привязке, в файле HomeController. cs usi ng Mi crosoft.AspNetCore.Mvc; u sing MvcModels . Models ; n ames pace MvcModels. Contr oll ers puЫic cl ass HomeCont rol ler : Controller p r ivate IReposi tor y r epository; puЫic HomeCont r oll er(IRepo sitor y repo) { repository = r e po; puЫic I ActionRes ult I ndex(int? i d ) { Person person; if (id.HasValue && (person = repository[id.Value]) != null) r eturn View(person) ; else{ return Not Found () ; p uЬlic ViewRes u lt Create() => View(new Person()) ; [ HttpPost] puЫic ViewResu lt Cr e ate(Pe r son mo d el) => View( " Inde x ", model) ; puЫi c ViewResult Displa ySummary ( [Bind(nameof(AddressSununary.City), Prefix = nameof(Person.HomeAddress))] AddressS u mma r y summary) => View(s u mmary) ; Первый аргумент атрибута Bi nd представляет собой разделяемый запятыми спи­ сок имен свойств, которые должны быть включены в процесс привязки моделей. В листинге 26.21 указано, что свойство Ci ty должно быть включено в процесс, а поскольку свойство Count r y в спис ке отсутству ет, оно будет исключено из процесса привязки моделей. Запустив приложение. запросив URL вида /Home/ C reat e, заполнив форму и от­ правив ее, вы заметите, что для свойства Country никакого значения не отобр аж ает­ ся, хотя оно было отправлено браузером как часть НТТР-запроса POS T (рис . 26.7). 1 D»pl•ySumm•<y Х ._ - > С 'Dlocall10st:S1861/нome/DisplaySummary 'tl - --'----~------·------·------- City : Paris Coun try: Рис. 26. 7 . Исключение свойства из процесса привязки моделей
Глава 26. Привязка моделей 813 Когда атрибут Bind применяется к параметру метода действия, он влияет только на экземпляры данного класса, которые привязываются для этого метода действия; все остальные методы действий продолжат попытки привязать все свойства, определенные типом параметра. Если нужно получить более обширный эффект, тогда атрибут Bind можно применить к самому классу модели, как демонстрируется в листинге 26.22 . Листинг 26.22 . Применение атрибута Bind в файле AddressSummary. cs using Microsoft.AspNetCore.Mvc; namespace MvcModels . Models { [Bind(nameof(City))] puЫic class AddressSummary puЫic string City { get ; set; puЫic string Country { get; set; Можно также исключать свойства явным образом, декорируя их атрибутом BindNever, как показано в листинге 26.23, но это означает, что новые свойства, до­ бавляемые в класс модели, будут включены в процесс привязки моделей, если только вы не забудете применить к ним данный атрибут. Листинг 26.23. Применение атрибута NeverBind в файле AddressSummary. cs using Microsoft . AspNetCore . Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace MvcModels . Models { puЫic class AddressSummary { puЫic string City { get; set; [BindNever] puЫic string Country { get; set; Совет. Имеется также атрибут BindRequired, который сообщает процессу привязки моде­ лей о том, что запрос должен включать значение для свойства. Ели запрос не содержит требуемого значения, тогда возникает ошибка проверки достоверности модели, как будет объясняться в главе 27. Привязка массивов и коллекций Процесс привязки моделей обладает рядом элегантных возможностей для привяз­ ки данных запросов к массивам и коллекциям, которые будут описаны в последую­ щих разделах. Привязка массивов Одной из элегантных возможностей стандартного связывателя модели является его поддержка параметров методов действий, которые являются массивами. В целях демонстрации добавьте в контроллер Home новый метод по имени Names (),код кото­ рого показан в листинге 26.24 .
814 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 26.24. Добавление метода действия в файле Homecontroller. cs using Microsoft . AspNetCore . Mvc ; using MvcModels.Models; namespace MvcModels . Controllers puЫic class HomeController : Contro ll er private I Repository repository; puЫic HomeController(IRepository repo) { repository = repo ; 11 .. . для кра~'кости другие методы действий не показаны .. . puЬlic ViewResult Names(string[] names) => View(names ?? new string[O]); Метод действия Names () имеет параметр типа строкового массива по имени names . Связыватель модели будет искать любые элементы данных под названи ем names и создавать массив, содержащий эти значения. Чтобы снабдить метод дейс­ твия представлением, создайте в папке Views/Home файл Razor по имени Names . cshtml и поместите в него разметку из листинга 26.25. Листинг 26.25. Содержимое файла Names. cshtml из папки Views/Home @model string [] @{ ViewBag . Title = "N ames "; Layout = "_Layout "; @if (Model.Length == 0) <form asp - action= " Names" method="post " > @for(inti=О;i<3;i++){ <div class= " form - group " > <label>Name @(i + 1) :</ l abe l > <input id= " names " name="names" class= " form- control " /> </div> <bu t ton type =" submit" class= "b tn btn - primary " >Submit</button> </form> else { <tаЫе c lass="t aЫe taЬl e- conden sed taЬle - bordered taЬle - striped " > @foreach ( string name i n Model) { <tr><th>Name : </th><td>@name</ td></tr > </tаЫе> <а a sp-a ction="N ames " class= " btn btn-primary">Back</a> Это представление отображает разное содержимое на основе количества элемен­ тов в модели представления. Если элементы отсутствуют, то представление выводит форму, которая содержит три идентичных элемента input:
<form method="post" action="/Home/Names"> <div class="form- group"> <label>Name 1:</label> Глава 26. Привязка моделей 815 <input id="names" name="names" class="form-control" /> </div> <div class="form-group"> <label>Name 2 : </label> <input id="names" name="names" class="form-control" /> </div> <div class="form-group"> <label>Name 3:</label> <input id="names" name="names" class= " form-control" /> </div> <button type="submit" class="Ьtn Ьtn-primary">Submit</button> </form> Когда форма отправляется, процесс привязки моделей обнаруживает, что целевой метод действия принимает массив, и производит поиск элементов данных, которые имеют такое же имя, как у параметра метода действия. В настоящем примере это означает, что все значения из элементов input с атрибутом name, установленным в names, будут собраны вместе для создания массива и его использования в качестве аргумента при вызове метода действия. Чтобы взглянуть на результат, запустите при­ ложение, перейдите на URL вида /Home/Names и заполните форму. Отправив форму, вы заметите, что отображаются все введенные значения (рис. 26.8). х 1 +- ~ С D loca lhost :51861/Home/Names 1--:-m~-1:--·------IBNalo,l"llllBBll ·,1 N::',. 1<- ~ С ~6'oco1h;;;,;-i0i/Нo;;of";~::°' -=~ - Joe Name. Joe i 1 Nam~ NameЗ: ~ l -;;eler_________ -- --- -·J •· · L______ . i Реtег ifi' L_______·~~~---------~--~---------~ Рис. 26.8. Привязка моделей для массивов Привязка коллекций Процесс привязки моделей способен создавать не только массивы . Он также поддерживает классы коллекций. В листинге 26.26 тип параметра метода действия Narnes () изменен на строго типизированный список.
816 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Листинг 26.26. Применение строго типизированной коллекции в файле HomeController.cs using Microsoft . AspNetCore.Mvc; using MvcModels . Models ; using System.Collections.Generic; namespace MvcModels . Contro llers { puЫic class HomeController : Controller private I Repository repository ; puЫic HomeController(IRepository repo) { repository = rep o ; // .. . для краткости другие методы действий не показаны . .. puЫic ViewResul t Names (IList<string> names) => View(names ?? new List<string>()); Здесь используется интерфейс IList<T>. Указывать конкретный класс реализа­ ции не нужно, хотя при желании это можно было бы сделать. В листинге 26.27 приве­ дено моди фицированное содержимое файла представления Names. cshtml, в 1<0тором задействован новый тип модели. Листинг 26. 27. Применение коллекции в качестве типа модели в файле Names . cshtml @model IList<string> @{ ViewBag.Title = "N ames "; Layout = " _Layout "; @if (Model . Coun t == О) <form asp - action= " Names " method="p ost " > @for(inti=О;i<З;i++){ <div class= "form- group " > <label>Name @(i + 1) :</label> <input id= " names " name= "names " class= " form- control " /> </div> <button type= "submit " class= "Ьtn Ьtn-pr ima ry " >Submit</button> </form> else { <tаЫе class= " taЫe taЫe - condensed taЫe - bordered t aЫe - striped"> @foreach (string name in Model) { <tr><t h >Name : </th><td>@name</td></t r> </tаЫе> <а asp-action= "N ames " class= " btn btn- primary " >Back</a> Функциональность действия Names не изменилась , но теперь оно способно рабо­ тать с классом коллекции вместо массива .
Глава 26 . Привязка моделей 817 Привязка коллекций сложных типов Индивидуаль ные знач ения данных можно также привязьmать к массиву сложных типов, что позволя ет собирать из единственного запроса множество объектов (таких как класс модели AddressSurnmary в примере). В листинг е 26.28 контроллер Home дополня ется новым методом действия по им ени Address () ,парам ет р которого пр ед­ ставляет собой списо1' объектов AddressS urnmary. Листинг 26.28 . Определение метода действия в файле HomeController. cs u sing Microsoft.AspNetCore . Mv c ; usi ng MvcModel s .Models; using System . Col l ections . Gene ri c ; namespace MvcModels . Contro llers { puЬlic class HomeController : Co n t r o l ler private IRepository repository ; puЫic HomeController(IRepos i to r y r epo ) { repository = repo ; // .. . для краткости другие методы действий не показаны ... puЬlic ViewResult Address(IList<AddressSummary> addresses) => View(addresses ?? new List<AddressSummary>()); Чтобы снабдить новый метод действия представлени ем, добавьте в папку Views/ Home файл по и м ени Address . cshtml с разметкой из листинга 26.29. Листинг 26.29. Содержимое файла Address. cshtml из папки Views/Home @model IList<AddressSumma r y> @{ ViewBag . Title = "Address "; Layout = " Layou t"; @if (Model. Count () == О) { <form asp - action= " Address " me t hod=" post" > @for(inti=О;i<3;i++){ <fieldset class= " form - gro up " > <legend>Address @(i + l)</legend > <div cl ass=" form- group"> <l abel>City:</labe l > <input name=" [@i] .City" class = " form- control " /> </div> <div class= " form - group " > <label>Country : </ l abe l > <input name=" [@i] .Country" cl as s=" form-control" /> </div> </fie l dset> <button type= " submit " clas s = "b tn bt n-prima r y " >Submi t</button> </form>
818 Часть 11 . Подробные с ведения об и нфрастру к туре ASP.NET Core MVC else ( <tаЫе class= " taЫe taЫe - condensed taЫe - bordered taЫe - striped"> <tr><th>City</th><th>Country</th></tr> @foreach (var address in Model) { <tr><td>@address . City</td><td>@address . Country</td></tr> </tаЫе> <а asp - action="Address" c l ass= " btn btn - primary " >Back</a> Представление визуализи рует элемент form , е сли элеме нты в коллекци и м одели отсутствуют. Элемент form состоит из пары элементов input, атрибуты name кото­ рых имеют префиксы в виде индексов массива : <form method= " post " action= " /Home/Address " > <fieldset class= " form - group " > <legend>Address l</legend> <div class= " form-group " > <label>City : </label> <input name="[O] .City" class="form-control" /> </div> <div class= " form - group " > <label>Country : </label> <input name="[O] .Country" class="form-control" /> </div> </fieldset> <fieldset class= " form - group " > <legend>Address 2</legend> <div class= " form - group " > <label>City:</label> <input name=" [ 1] . City" class=" form-control" /> </div> <div class= " form - group " > <label>Country : </labe l > <input name=" [1] .Country" class="form-control" /> </div> </fieldset> <fieldset class= " form - group " > <legend>Address 3</legend> <div class= " form - group " > <label>City : </label> <input name=" [2] .City" class="form-control" /> </div> <div class= " form - group"> <label>Country:</label> <input name=" [2] .Country" class="form-cont r ol" /> </div> </fieldset> <button type= " submit " class= " Ьtn Ьtn - primary " >Submit</button> </form>
Глава 26. Привязка моделей 819 Когда форма отправляется, связыватель модели выясняет, что необходимо создать коллекцию объектов AddressSummary, и использует префиксы индексов массива в атрибутах name, чтобы получить значения для свойств этих объектов. Свойства с префиксом [О ] применяются для первого объекта AddressSumary, свойства с пре­ фиксом [1] - для второго объекта AddressSumary и т.д. Представление Address . cshtml определяет элементы input для трех таких индексированных объектов и отображает их, если коллекция модели содержит эле­ менты. Перед тем, как можно будет продемонстрировать это в работе, понадобится удалить атрибут BindNever из класса модели AddressSummary, иначе связыватель модели проигнорирует свойство Country (листинг 26.30). Листинг 26.30 . Удаление атрибута BindNever в файле AddressSurnmary. cs using Microsoft . AspNetCore.Mvc; using Microsoft . AspNetCore . Mvc . ModelBinding ; namespace MvcModels . Models { puЫic class AddressSummary { puЫic string City { get; set; // [BindNever] puЫic string Country { get; set ; Запустив приложение и перейдя на URL вида /Home/Address, можно понаблю­ дать за работой процесса привязки для коллекций специальных объектов. Введите значения в полях для города и страны, после чего щелкните на кнопке Submit , чтобы отправить форму серверу. Процесс привязки моделей найдет и обработает индексируемые значения данных и использует их для создания коллекции объектов AddressSummary, предоставляе­ мой методу действия, который затем применяет удобный метод View (),чтобы пере­ дать их обратно представлению с целью отображения (рис. 26.9). 1 .AddttSS Х Address 1 City : Lo ndon Count ry: United Кingd om Cou ntry : Germ eny ~-~ ::____с _ D!.0:·~~~~:~1:~у~~'::~/д!~ '.':'~ ·- _ -~ "-- 1 1 1 City Lond on Paris Berlin 1 1111 1 Country Un lted Kingdo 111 Fraпce Germ any =' L----------------~--------~ _ ____ ____ __ J Рис. 26.9 . Привязка коллекций специальных объектов
820 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Указание источника данных привязки моделей Как объяснялось в начале главы , стандартный процесс привяз1<и моделей ищет значения в трех местах: отправленные данные формы, переменные маршрутизации и строки запросов . Стандартная последовательность поиска не всегда полезна , либо потому. что дан­ ные должны всегда поступать из специфической части запроса, либо потому, что нужно использовать источник данных, в котором поиск по умолчанию не произво­ дится . Средство привязки моделей включает набор атрибутов. которые применяются для переопределения стандартного поведения поиска и описаны в табл. 26.3 . Таблица 26.3 . Атрибуты для источников данных привязки Имя Описание FrornForrn Этот атрибут используется для выбора данных формы в качестве источника данных привязки. По умолчанию для нахождения значения данных формы применяется имя параметра, но такое поведение можно изменить с исполь­ зованием свойства Narne, которое позволяет указывать другое имя FrornRout e Этот атрибут применяется для выбора системы маршрутизации в качестве источника данных привязки. По умолчанию для нахождения значения данных маршрута используется имя параметра, но такое поведение можно изменить с применением свойства Narne, которое позволяет указывать другое имя FrornQue r y Этот атрибут используется для выбора строки запроса в качестве источника данных привязки. По умолчанию для нахождения значения строки запроса при­ меняется имя параметра, но такое поведение можно изменить с использовани­ ем свойства Narne, которое позволяет указывать другой ключ строки запроса FrornHe a der Этот атрибут применяется для выбора заголовка запроса в качестве источ­ ника данных привязки. По умолчанию имя параметра используется как имя заголовка, но поведение можно изменить с применением свойства Narn e, которое позволяет указывать другое имя заголовка FrornBody Этот атрибут используется для указания, что в качестве источника данных привязки должно применяться тело запроса. Требуется в ситуациях , когда из запросов необходимо получать данные, не закодированные как данные формы, что происходит в контроллерах API Выбор стандартного источника данных привязки Атрибуты Frorn Fo r rn , Frorn Ro u t e и FrornQu ery позволяют указывать , что данные привязки модели будут получаться от одного из стандартных местоположений, но без нормальной последовательности поиска. Ранее в главе использовался следующий URL: / Home/ Index /З?i d=l Приведенный URL содержит два возможных значения , которые могут применяться для параметра i d метода действия Index () в контроллере Н оте. Система маршрути­ зации назначит последний сегмент URL переменной по имени id, которая определена в шаблоне URL внутри класса St a r tup. Строка запроса также содержит значение id. Стандартный шаблон поиска означает, что значения привязки модели будут извле­ каться из данных маршрутизации, а строка запроса игнорируется.
Глава 26. Привязка моделей 821 Чтобы изменить тако е поведение, в листинге 26.31 к методу действия применя­ ется атрибут FromQuery . Ради простоты также удалены вс е остал ьные методы дейс­ твий, которые были определены в предшествующих примерах. Листинг 26.31. Выбор строки запроса для привязки модели в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using MvcMode l s . Models ; namespace MvcModels . Contro l lers puЫic class HomeController : Contro l ler private IRepos i tory repository ; puЫic HomeController(IReposito ry r epo) { repository = repo ; puЫic IActionResult Index ( [FromQuery] int? id) { Person person ; if (id.HasValue && (person = r eposi t ory[id.Value]) != null) { ret u rn View(person) ; else { return NotFound() ; Здесь атрибут FromQuery пр именяется к параметру id, т.е . во время поиска про­ цессо м привязки моделей значения данных для i d будет использоваться только стро­ ка запроса. Совет. При указании источника данны х привязки моделей , такого как строка запроса, по­ пре жнему можно осуществлять привязку сложных типов. Для каждого простого свойства в типе параметра процесс привязки моделей будет искать ключ строки запроса с тем же самым именем . И спользо вание заголовко в в качеств е источников данных привязки Атр ибут FromHeader позволя ет заде йствовать заголовки НТГР-запроса как источ ­ н ики данных привя зки. В листинге 26.32 в контроллер Home добавлен простой м етод дей ствия, который получает привязку параметра с прим ене нием данных из стандар­ тного з аголовк а НТГР-запро са. Листинг 26.32. Привязка модели из заголовка НТТР-запроса в файле HomeController.cs using Microsoft . AspNetCore . Mvc ; using MvcModels . Models ; namespace MvcModels . Control l ers puЫic class HomeController : Controller private IRepository repository ;
822 Часть 11 . Подробные сведения об инфраструктуре ASP.NET С аге MVC puЫic HomeController(IRepository repo) { repository = repo; puЫic IActionResult Index( [FromQuery] int? id) { Person person ; if (id .H asValue && (person = repository[id.Value]) ! = null) { return View(person) ; else { return NotFound(); puЬlic string Header ( [FromНeader] string accept) => $ 11 Header: {accept} 11 ; Метод действия Header () определяет параметр accept , знач ение которого будет браться из заголовка Accept в текущем запросе и возвращаться в качестве р езульта ­ та метода. Запустив приложение и запросив URL вида /Home/Header, вы получит е прим е рно такой результат (точный результат может отличаться в зависимости от ис­ пользуем ого браузе ра): Header : text/html , application/xhtml+xml , application/xml; q=0 .9,image/webp, */*;q=0 .8 Не все имена НТГР-заголовков могут легко выбираться. полагаясь на имя парамет­ ра метода действия, потому что система привязки моделей не пр еоб разует соглаше­ ния по именованию С# в соглашения. принятые в НТГР-заголовках . В таких ситуаци­ ях вы должны конфигурировать атрибут FromHeader с применением сво йств а Name для указания имени заголовка (листинг 26.33). Листинг 26.33. Указание имени заголовка в файле HomeController. cs using Microsoft . AspNetCore . Mvc; using MvcModels . Models ; namespace MvcModels . Contro l lers puЫic class HomeController : Controller private IRepository repository ; puЫic HomeController(IRepos i tory repo) { repository = repo ; puЫic IActionResult Index([FromQuery] int? id) { Person person ; if (id . HasValue && (person = repository[id .Val ue]) ! = null) { return View(person) ; else { return NotFound() ; puЬlic string Header( [FromНeader(Name = 11 Accept-Language") ] string accept) => $ 11 Header: {accept} 11 ;
Глава 26. Привязка моделей 823 Использовать Accept -Language для имени параметра С# нельзя, и связыва­ тель модели не будет автоматически преобразовывать имя вроде AcceptLanguage в Accept - Language, чтобы оно соответствовало заголовку. Взамен с помощью свойс­ тва Name атрибут :конфигурируется та:к, что он соответству ет правильному заголов­ ку . Запустив приложение и запросив URL вида /Home/Header, вы получите ответ, подобный показанному ниже, :который будет варьироваться на основе региональных настроек : Header: en-US ,en;q=0 .8 Привязка сложных типов из заголовков Несмотря на то что это редко требуется , привязку сложных типов можно выпол­ нять. используя значения заголовков, за счет применения атрибута FromHeader :к свойствам :класса модели. Добавьте в папку Models файл по имени HeaderModel. cs и определите в нем :класс, представленный в листинге 26.34. Листинг 26 .34 . Содержимое файла HeaderModel. cs из папки Models using Microsoft . AspNetCore . Mvc; namespace MvcModels . Mode ls { puЫic class HeaderModel { [FromHeader] puЫic str ing Accept { get; set; } [FromHeader(Name = " Accept - Language " )] puЫic string AcceptLanguage { get ; set ; [FromHeader(Name = " Accept-Encoding " )] puЫic string AcceptEncoding { get; set ; В :классе определены три свойства, каждое из которых декорировано атрибутом FromHeader . В двух атрибутах с помощью свойства Name указываются имена заго­ ловков , которые невозможно выразить как имена параметров С# . В листинге 26.35 приведен обновленный метод действия Header () контроллера Ноте, теперь получа­ ющий объект HeaderModel. Листинг 26 .35. Использование класса HeaderModel в файле HomeController. cs using Microsoft . AspNetCore . Mvc ; using MvcModels . Models ; namespace MvcModels . Controllers puЫic class HomeController : Controller private IRepository repository ; puЫic HomeController(IRepository repo) { repository = repo ; puЫic IActionResult Index( [FromQuery] int? id) { Person person; if (id . HasValue && (person = repository[id . Value]) != null) { return View(person) ;
824 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC else{ return NotFound() ; puЬlic ViewResult Header(HeaderModel model) => View(model); Чтобы завершить пример , добавьте в папку Views/Home файл представления по имени Header . cs h tml с разметкой и з листинга 26.36. Листинг 26.36. Содержимое файла Header. cshtml из папки Views/Home @model Heade r Model @{ ViewBag.Title = "Header s "; Layout = " Layout"; <tаЫе class= "t aЫe taЬle - condensed taЬle -b ordered taЫe - striped " > <tr><th>Accept : </th><td>@Mode l. Acce pt</ t d></ t r> <tr><th >Accept - Encoding : </th ><t d>@Mode l.Accep t Encoding</td></tr> <tr><th >Accept -La n guage : </th ><td>@ Model . Acce p t Lang u age</td></tr> </tаЫе> Процесс привязки моделей будет исследов ать свойства сло жных типов в поис­ ке атрибутов , опи санных в табл . 26.3 . Это дает возможность посредством атрибута FromH eader определить сложный тип, свойства которого являются моделью , привя­ зываемой и з заголов1юв, в ч ем л егко удостов е риться, запустив прило жение и запро ­ сив URL вида / Home/Header (рис . 26 . 10). 1 Head•rs Х +- .. .. ; ., С [j locall1ost:s1861/н-;~;;н-;ade;:------------------·--~ Е 1 · ---· -- Ассер!: text/html.application/xhtml+xml,application/xml ;q=0 .9,imag e/webp ,•1 •;q=0 .8 Accept-Encoding : gzip , deflate. sdch Accept-Language: en -US,en ;q=0 .8 Рис. 26.10. Привязка сложного типа из заголовков запроса Использование тел запросов в качестве источников данных привяз ки Не все данные, отправляемые клиентами , посылаются в виде данных формы, как происходит в ситуации, когда кли ент JavaScript отправляет данные JSON контролле­ ру API . Атрибут F r omBody указыв ает, что тело з апроса долж но быть де кодировано и применяться в качеств е источник а данных привязки модели. В листинге 26.37 пока ­ заны новые методы действий Body ( ) ,которые демонстрируют, как это работа ет.
Глава 26 . Привязка моделей 825 Листинг 26.37. Добавление методов действий в файле HorneController. cs using Microsoft . AspNetCore.Mvc ; using MvcModels . Models; namespace MvcModels . Controllers puЫic class HomeController : Controller private IRepository repository ; puЫic HomeController(IRepository repo) { repository = repo; puЬlic IActionResult Index([FromQuery] int? id) { Person person ; if (id.HasValue && (person = repository[id.Value]) != null) { return View(person); else { return NotFound () ; puЫic ViewResult Header(HeaderModel model) => View(model) ; puЫic ViewResul t Body () => View () ; [HttpPost] puЫic Person Body ( [FrornВody] Person rnodel) => rnodel; Параметр метода Body (), принимающего запросы POST, декорирован атрибутом FromBody, т.е. содержимое тела запроса будет декодировано и использовано для при­ вязки модели. Как объяснялось в главе 20, инфраструктура MVC имеет расширяемую систему для работы с разными форматами данных, но по умолчанию она настроена на обработку только данных JSON. Добавьте в прилож ение пакет jQuery, отредактировав файл bower . j son согласно листингу 26.38 . Листинг 26.38 . Добавление пакета jQuery в файле bower. j son "name": "asp.net", " private ": true, " dependencies ": { "bootstrap": "3 .3 .6", "jquery": "2. 2 . 4" Чтобы снабдить метод действия требуемыми данными, добавьте в папку Views/ Horne ф айл по имени Body. cshtrnl и поместите в него содержимое, приведенное в листинге 26.39 .
826 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 26.39. Содержимое файла Body. cshtml из папки Views/Horne @( ViewBag . Title = " Address "; Layout =" Layout " ; @section scripts { <script src="/liЬ/jquery/dist/jquery.min.js"></script> <sc r ipt type= " text/javascript " > $(document) . ready(function () { $( " button " ) . click(function (е) $ . ajax( " /Home/Body ", { method : " post ", contentType : " app l ication/ j son ", data: JSON . stringify({ firstName: " ВоЬ ", lastName : " Smith " )), success: function (data) $("#firstName") .text(data .firstName) ; $( " #lastName " ) . text(data . lastName) ; ) )); )); )); </script> <tаЫе class= " taЬle taЫe-condensed taЫe - bordered taЬle-striped"> <tr><th>First Name:</th><td id="firstName"></td></tr> <tr><th>Last Name:</th><td id="lastName"></td></tr> </tаЫе> <button class="Ьtn Ьtn-primary " >Submit</button> Для простоты представление содержит встраиваемый код JavaScript, который с помощью jQuery посылает НТГР -запрос POST, соде ржащий данные JSON , на URL вида /Home/Body, когда производится щелчок на элементе button . Сервер кодирует объект, созданный с применением привязки модели, и отправляет его обратно кли­ енту как закодированные данные JSON. Запустив приложение, запросив URL вида /Home/Body и щелкнув на кнопке Submit, можно просмотреть результат (рис . 26 . 11). Совет. Не весь клиентский код JavaScript требует использования атрибута FromBody. В рас­ смотренном примере намеренно не применялся удобный метод jQuery для отправки Аjах­ запросов POST, потому что он кодирует информацию как данные формы. Взамен исполь­ зовался другой метод, который позволил послать данные JSON. Атрибут FromBody можно прим енять для привязки только одного пар аметра ме­ тода действия, и в случае использования этого атрибута более одного раза в отдельно взятом методе генерируется исключение. Если нужно получить несколько объектов модели из тела запроса, тогда придется создать простой класс п ередачи данных, ко­ торый имеет все необходимые свойства и применя ет содержащиеся в нем данные для создания объектов, требуемых внутри метода действия.
Глава 26 . Привязка моделей 827 1 Addrш х 1 ~ -"!- ,~ · CJ localhost :S1861 / Home/Bo !-·---·-------- --------- ·~> С . CJ localhost:51861 / H()me/Body First Name : First Name: ВоЬ Last Name : [•+11 ________, Smil h '-------· ------------------ Рис. 26.11. Использование тела запроса для привязки моделей Резюме В настоящей глав е рассматривался процесс привязки моделей, который позволя ет предоставлять методам действий необходимые аргументы, используя значения дан­ ных из обрабатываемого НТГР-запроса. Было показано, как привязывать простые и сло жные типы, каким образом иметь дело с массивами и коллекциями, а также как управлять процессом привязки моделей за счет применения атрибутов к параметрам методов действий или свойствам классов моделей. В следующ ей главе будет описано средство проверки достоверности моделей.
ГЛАВА 27 Проверка достоверности v моделеи в предыдущей главе было показано, как инфраструктура MVC создает объекты моделей из НТТР-запросов с помощью процесса привязки моделей . В ней пов­ семестно предполагалось, что предоставляемые пользователем данные были допус­ тимыми. Реальность же такова, что пользователи часто вводят данные, которые не являются допустимыми и не могут использоваться, поэтому темой настоящей главы будет проверка достоверности моделей. Проверка достоверности моделей представляет собой процесс выяснения, что дан­ ные, полученные приложением, подходят для привязки моделей, а если это не так, то пользователям предоставляется полезная информация. которая помогает устранить проблему. Первая часть этого процесса - проверка полученных данных - является од­ ним из основных способов предохранить целостность модели предметной области. Отклонение данных , которые не имеют смысла в контексте предметной области , мо­ жет предотвратить возникновение странных и нежелательных состояний в прило­ жении. Вторая часть - помощь пользователям в корректировке проблемы - важна в равной степени. Без такой информации и обратной связи, необходимой для вза­ имодействия с приложением, пользователи будут недовольны и озадачены. В обще­ доступных приложениях это означает, что пользователи просто прекратят ими поль­ зоваться. В корпоративных приложениях это означает нарушение рабочего потока пользователей. Ни тот, ни другой исход не является желательным, но к счастью инф­ раструктура MVC предоставляет широкую поддержку проверки достоверности моде­ лей. В табл . 27. l прив едена сводка, позволяющая поместить проверку достоверности моделей в контекст. Таблица 27 .1. Помещение проверки достоверности моделей в контекст Вопрос Что это такое? Чем она полезна? Ответ Проверка достоверности моделей - это процесс выяснения, что данные, предоставленные в запросе, пригодны для применения в приложении Пользователи не всегда вводят допустимые данные. Использование таких данных в приложении может приводить к неожиданным и нежелательным ошибкам
Вопрос Как она используются? Существуют ли какие-то с к рытые ловушки или ограничения? Глава 27. Провер к а достоверности моделей 829 Окончание табл . 27. 1 Ответ Контроллеры исследуют результат процесса проверки достовер­ ности, а посредством дескрипторных вспомогательных классов в представления, отображаемые пользователям, включаются отклик проверки . Проверка достоверности автоматически выполняется во время процесса привязки моделей. Она обычно дополняется специальной проверкой в классе контроллера или за счет приме ­ нения атрибутов проверки достоверности Важно протестировать эффективность кода проверки достовер ­ ности, удостоверившись в том , что он способен справляться с пол­ ным диапазоном значений , которые может получать приложение Существуют ли альтернативы? Нет, проверка достоверности моделей тесно интегрирована в ASP.NET Саге MVC Изменилась ли она по сравнению с версией MVC5? Базовый подход к выполнению проверки достоверности остался таким же , как в предшествующи х версиях MVC, но изменились некоторые лежащие в основе классы и интерфейсы В табл. 27.2 приведена сводка для настоящей главы. Таблица 27.2. Сводка по главе Задача Явная проверка достовер­ ности модели Генерация сводки по ошиб­ кам проверки достоверности Изменение стандартных со­ общений привязки моделей Генерация ошибок проверки достоверности на уровне СВОЙСТВ Генерация ошибок проверки достоверности на уровне модели Определение самопроверя­ емой модели Создание специально- го атрибута проверки достоверности Выполнение проверки досто­ верности на стороне клиента Выполнение удаленной про­ верки достоверности Решение Используйте объект ModelState для регист­ рации ошибок проверки достоверности Примените атрибут asp - validation- swnmary к элементу div Переопределите функции сообщений в постав­ щике сообщений привязки моделей Примените атрибут asp - val i dation - fo r к элементу span Используйте объект ModelState для регист­ рации ошибок проверки достоверности , которые не ассоциированы со специфическими свойства­ ми , и укажите значение ModelOnly для атрибу­ та asp - validation - summary в элементе div Применяйте атрибуты проверки достоверности данных к свойствам модели Реализуйте интерфейс I Mode l Validato r Используйте пакеты ненавязчивой проверки достоверности jQuery Определите метод действия для выполнения проверки достоверности и примените атрибут Remote к свойству модели Листинг 27.1-27.11 27.12 27 .13 27.14 27.15, 27.16 27.17, 27 .18 27.19, 27 .20 27 .21 , 27.22 27.23, 27.24
830 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Подготовка проекта для примера Создайте новый проект типа Empty (Пустой) по имени ModelValidatio n с исполь­ зованием шаблона ASP.NET Core Web Application (.NЕТ Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dependencies ф айла proj ect . j son и настройте инструментарий Razor в разделе tools, как показано в листинге 27.1 . Разделы, которые не нужны для данной главы , понадобится удалить. Листинг 27 .1. Добавление пакетов в файле proj ect. j son "dependencies ": { "Microsoft.NETCore.App": }, "version ": " 1.0.0 ", "type": "platform " }, " Microsoft . AspNetCore . Diagnostics ": "1.0 .0", "Microsoft.AspNetCore.Server.IISintegration": "1.0. 0 ", " Microsoft .AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ", " Microsoft .Ex tensions . Logging .C onso l e ": " 1 .0. О ", "Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.StaticFiles": 11 1.0 .0 11 , "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0 .0 -preview2-final", "type": "build" " tools": "Microsoft.AspNetCore.Razor.Tools": "1.0 .0 -preview2-final", " Microsoft . AspNetCore .Se rver .IISintegration.Tools ": " 1 .0 .0-preview2-final " }, "frameworks": { "net coreappl.0 ": "imports ": [ "do tnet5 . б ", "p ortaЫe -n et45+win8"] }, "b uildOptions ": "emi tEntryPoint ": tr ue, "pr eserveCompilationContext ": true }, " runtimeOptions ": { " configProperties ": { " System .GC .S e rve r ": true} В листинге 27.2 показан код класса Startup, который конфигурирует средства, предоставляемые пакетами NuGet.
Глава 27. Проверка достоверности моделей 831 Листинг 27.2 . Содержимое файла Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extens i ons . Dependencyinjection ; namespace ModelValidation puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Создание модели Создайте папку Models и добавьте в нее файл класса по имени Appointment. cs с определением, приведенным в листинге 27.3 . Листинг 27.3 . Содержимое файла Appointment. cs из папки Models using System; using System . ComponentModel . DataAnno t ations ; namespace ModelVa lida tion . Models { puЫic class Appointment { puЫic string ClientName { get; set; [UIHint ( " Date " )] puЫic DateTime Date { get; set ; puЫic bool TermsAccep ted { get ; set; В классе Appoi ntment определены три свойства, и с помощью атрибута UIHint указано, что свойство Da te должно выражаться как дата без компонента времени. Создание контроллера Создайте папку Controllers, добавьте файл класса по имени HomeController. cs и поместите в него определение контроллера из листинга 27.4, который оперирует с классом модели Appointment . Листинг 27.4 . Содержимое файла HomeController. cs из папки Controllers using System ; using Microsoft.AspNetCore . Mvc; using ModelValidation.Mode l s ;
832 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC namespace ModelValidation . Controllers { puЫic class HomeController : Controller puЫic IActionResult Index() => View ( " MakeBooking ", new Appointment { Date = DateTime . Now ) ) ; [ HttpPost] puЫic ViewResult MakeBoo king(App oin tmen t appt) => View ( " Completed" , appt) ; Действие Index ви зуализи рует представ ление MakeBooking с новым объектом Appointment в к ачестве модели представления. Метод действия MakeBooking () бо­ лее интересен , т.к . в нем будет выполняться провер ка достоверности модели. На заметку! Пример прило жения настолько прост, что в нем да же не определено х ранилище и не добавлен код для сохранения объектов Appointment, которые выпускаются про ­ цессом привязки моделей. Тем не менее , важно иметь в виду, что главной причиной про­ верки достоверности модели является предотвращение попадания в х ранилище непра­ вильных или бессмысленных данных с последующим возникновением проблем (либо при попытке сохранения данных·, либо при попытке их обработки в более позднее время). Создание компоновки и представлений Для ряда примеров в этой главе требуется простая компоновка. Со здайте папку Views/Shared и добавьте в нее файл _Layout. cshtml, с одержим о е которого пок а­ зано в листинге 27.5 . Листинг 27.5 . Содержимое файла _Layout. cshtml из папки Views/Shared < ! DOCTYPE html> <html> <head> <meta charset= " utf- 8 " /> <meta name= " viewport " conten t="width=device-wi dth " /> <title>Model Validation</title> <link asp - href - inc l ude= " /l i Ь/bootstrap/dist/**/*.min.css" rel= " stylesheet " /> @RenderSection( " scripts ", false) </head> <body class= "panel-body " > @RenderBody () </body> </html> Чтобы снабдить методы действий представлениями , создайте папку Views/Home и добавьте в нее файл по имени MakeBooking. cshtml с разметкой из листинга 27.6. Листинг 27.6 . Содержимое файла МakeBooking. cshtml из папки Views/Home @mode l Appointment @{ Layout = " _Layo ut"; <div class= " bg-primary panel - body " ><h2>Book an Appointment</h2></div>
Глава 27 . Проверка достоверности моделей 833 <form class= " panel - body " asp-action="MakeBooking " method=" post " > <div class= "form-g roup "> <label asp - for= " ClientName " >Your name : </labe l > <input asp - for= " Client Nam e " clas s=" for m-con trol " /> </div> <div class= " form-group " > <label asp-fo r = "D ate " >Appointme nt Date : </label> <input asp- for="Date " type="text " asp- format= "{ O:d)" class= "form- control " /> </div> <div class= " ra d io form-group " > <input asp - fo r="T ermsAccepted " /> <label asp - for="TermsAccepted"> I accept the terms & conditions</label> </div> <but t o n type= " submit " class ="Ьtn Ьtn-primary " >Make Booking</button> </form > Когда форма, содержащаяся в файле Index . cshtml, отправляется обратно при­ ложению , метод действия MakeBoo k ing () отображает детали созданной пользовате­ лем встречи с применением представления Completed. cshtml из папки Views/Home (листинг 27.7) . Листинг 27.7 . Содержимое файла Completed.cshtml из папки Views/Home @model Appointment @{ Layout = " Layout "; <div class= " bg -success panel-body " >< h2 >Your Appo intment</h2 ></div> <tаЫе class= "taЫe taЬle - bordered"> <tr> <th>Your name is : </th> <td>@Mode l. Cli entName</td > </tr> <tr> <th>Your appontment date i s : </th > <td>@Mode l. Date . ToString("d")</td> </tr> </tаЫе> <а class= " btn btn-success " asp-action= "I ndex " >Make Anothe r Appointment</a> При стилизации НТМL-элементов представ ления полагаются на СSS-пак ет Bootstrap. Создайте в корневой папке проекта файл bower. j son с использованием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па­ кет Bootstrap в раздел dependenci es (л истинг 27.8). Кроме того, добавьте также и пакет jQuery, который понадобится позже в глав е. Листинг 27.8 . Добавление пакета Bootstrap в файле bower. j son " name" : " asp.net ", "private ": true , " dependencies ": { "bootstrap": 11 3.3 .6 11 , "jquery": "2. 2 .4"
834 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Последний подготовительный шаг связан с созданием файла Viewirnports. cshtml в папке Views, в котором настраиваются встроенные дескрипторные вспо­ могательные классы для применения в представлениях Razor и импортируется про­ странство имен модели (листинг 27.9). Листинг 27.9. Содержимое файла_Viewimports. cshtml из папки Views @using ModelValidation.Models @addTagHelper * , Micr o so ft .AspNetCore.Mvc.TagHelpers Как вы уже наверняка догадались, пример основан на создании встреч. Взглянуть на него в работе можно, запустив приложение и запросив стандартный URL. Ввод в элементах формы информации о встрече и щелчок на кнопке Make Booking (Создать встречу) приведет к отправке данных серверу. который выполнит процесс привязки моделей для создания объекта Appo int ment, детали которого затем визуализируются с использованием представления Completed. cshtml (рис . 27.1) . Cj Mod•I Vo lidotio n С 10 localhost:S5750 Your name: ВоЬ Ap poin tment Date: 12/12/2020 12/12/2 020 ] ---- ·- ------ ----------·-·------ Make .Booking ' ~ ~ ~ __ J Рис. 27 .1. Запуск примера приложения Необходимость в проверке достоверности модели Проверка достоверности модели - это процесс принудительного применения требований, которые приложение налагает на данные, получаемые от клиентов. Без проверки достоверности приложение будет пытаться оперировать с любыми получен­ ными данными, что может привести к генерации исключений и непредвиденному по­ ведению, которое отразит немедленные или долговременные проблемы, постепенно накопившиеся по мере того, 1<ак хранилище наполняется неправильными, незавер­ шенными или злонамеренными данными.
Глава 27. Проверка достоверности моделей 835 В настоящ ее время пример прилож ения будет принимать любые данные, которые отпр авляет пользоват ель . Чтобы предохранить целостность приложения и м одели предм етно й обл асти, должны быть удовлетворены три перечисленных ниже условия , пре жде чем ста нет известно , что поль з ователь предоставляет приемл е мый объект Appointment: • пользоват ель должен предоставить имя: • пользоват ель должен пр едоставить дату, относящуюся к будущ е му; • пользователь должен отм етить фл ажок для принятия условий. В последующих разделах демонстрируется использование проверки достоверности модел ей для навя зывания указанных требований за сч ет контроля получаемых при­ лож ением данных и предоставления пользователям обратной связи, когда приложе­ ние не может работать с данными, которые они отправили . Slвная проверка до стов ер ности модели Самый прямой способ проверки достоверности модели предусматривает е е вы­ полнение в методе действия. В листинге 27.10 показан новый метод действия MakeBooking () с добавленными явными проверками для каждого свойства, опреде­ ляемого классом App o i ntmen t . Листинг 27.10. Явная проверка достоверности модели в файле HorneController. cs us ing System; using Mi crosoft . AspNetCo r e .M vc ; u sing ModelValidat i o n. Mode ls; using Мicrosoft.AspNetCore.Mvc . ModelBinding; namespace ModelVali dati on . Control lers { puЬlic class HomeContr oller : Control l e r puЫic IActionRes ult Index ( ) => View( "MakeBooking", new Appoi ntment { Date = Dat eTime .Now )) ; [HttpPost] puЫic ViewResult MakeBooking(Appointment appt) { if (string . IsNu110rEmpty(appt.C1ientName)) { ModelState.AddМodelError(nameof (appt.ClientName), "Please enter your name"); //Введите свое имя if (ModelState. GetValidationState ( "Date") == ModelValidationState.Valid && DateTime.Now > appt.Date) ModelState.AddМodelError(nameof(appt.Date), "Please enter а date in the future") ; / / Введите дату, относящуюся к будущему if (!appt.TermsAccepted) { ModelState.AddМodelError(nameof (appt.TermsAccepted), "You rnust accept the terms"); 11 Вы должны принять условия
836 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC if (ModelState.IsValid) { return V.iew ( 11 Completed 11 , appt) ; else { return View(); В коде проверяются значения, которые связыватель модели присвоил свойств ам объекта параметра. Любые обнаруженные ошибки регистрируются с применение м объекта ModelStateDictionary, который возвращается свойством Model Sta te, унаследованным из базового класса Cont r ol ler. Как подсказывает его имя, класс ModelStateDictionary - это словарь, кото­ рый используется для отслеживания деталей состояния объекта модели, с акцентом на ошибках проверки достоверности . В табл. 27.3 описаны наиболее важны е члены класса ModelStateDictionar y. Таблица 27.З. Избранные члены класса ModelStateDictionary Имя AddМode lE rror(property , message) GetValidationState(property) IsValid Описание Этот метод применяется для регистрации ошиб ки проверки достоверности модели, связанной сука­ занным свойством Этот метод используется для выяснения, связаны ли со специфическим свойством ошибки проверки достоверности модели; результат выражается как значение перечисления ModelValidationState Это свойство возвращает true, если все свойства модели допустимы, и false в противном случае В качестве примера применения ModelStateDictionary в згляните, как выпол­ нялась проверка достоверности свойства ClientName: if (string . IsNullOrEmpty( app t . ClientName)) { ModelState . AddModelError(narneof(appt.ClientNarne) , 11 Pl ease enter your name 11 ); Одна из целей приведенного примера связана с гарантированием того, что пользо­ ватель предоставляет значение для данного свойства, поэтому здесь используется ста­ тический метод string . IsNullOrEmpty () для проверки значения свойства , которое процесс привязки моделей извлек из запроса . Если свойство ClientName равно null или пустой строке, тогда известно , что цель проверки достоверности не была достиг­ нута. В таком случае посредством метода Mode l State . AddModelError () регистри­ руется ошибка проверки достоверности с указанием имени свойства (Cli entName) и сообщения, которое будет отображаться пользователю для объяснения природы про­ блемы (Please en ter your name (Введите свое имя)).
Глава 27. Проверка достоверности моделей 837 Система привяз ки м оделей также применя ет объект ModelStateDi ctionary для регистрации любых проблем с нахождением и присваивание м значений свойств ам модели. М етод GetVa l idation State () используется с цел ью выяснения , были ли з арегистриров аны к аки е -то ошибки для свойства модели, либо из процесс а привязки моделей, либо по причине вызова метода AddModelError () во время явной проверки достоверности в методе действия. Метод GetVa l idationState () возвращает значе­ ни е пер ечисл ения ModelVal idationState , которое описано в табл. 27.4 . Таблица 27.4 . Значения перечисления ModelValidationState Имя Описание Unvalidated Это значение указывает, что в отношении свойства модели проверка достоверности не выполнялась, обычно из-за отсутствия в запросе значения, которое бы соответствовало имени свойства Valid Invalid Sk i pped Это значение указывает, что значение в запросе, ассоциированное со свойством, является допустимым Это значение указывает, что значение в запросе, ассоциированное со свойством, является недопустимым и применяться не должно Это значение указывает, что свойство модели не было обработано. Обычно это говорит о наличии настолько большого количества ошибок проверки достоверности , что продолжать проверку не имеет смысла Для свойства Da te выполняется проверка. сообщил ли процесс привязки моделей о пробле м е во время пр еобразования отправленного браузером значения в объ ект DateTi me : if (ModelState.GetValidationState("Date") == ModelValidationState . Valid && DateTime.Now > appt .Date) { ModelState . AddMode l Error(nameof(appt . Date) , " Please enter а date in the future"); Цель пров ерки достове рности для свойства Da t e - обеспечить предоставле­ ни е пользов ателем допу стимой даты в будущем. Метод GetValidationState () использу ется для выяснения, смог ли процесс привяз ки моделей преобразовать значение из з апроса в объ ект DateTime, за счет проверки на предм ет значения Model Valida tionSta te . Valid. В случае допустимой даты выполняется проверка, относится ли она к будущему, и если нет, то посредством метода AddМode lError () регистрируется пробл ема проверки достоверности. После того, как все свойства объ е кта модели проверены , с помощью свойства ModelState . IsValid выясняется, возникали ли ошибки. Данное свойство возвраща­ ет true, если во время проверок вызывался метод Mo del. State . Ad dMod elEr r or ( ) или у связывателя модели возникали проблемы с созданием объекта Appointme nt : if (ModelState. IsValid) { return View ( " Complete d", appt) ; else( return View () ;
838 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Объект Appointment является допустимым, если свойство IsValid возвраща­ ет true, в случае чего метод действия визуализирует представление Completed. cshtml. Если же свойство IsValue возвращает false, тогда есть проблема проверки достоверности. которая обрабатывается путем вызова метода View () для визуализа­ ции стандартного представления. Отображение пользователю ошибок проверки достоверности Обработка ошибки проверки достоверности за счет вызова метода View () может показаться странным подходом. но данные контекста, которыми инфраструктура MVC снабжает представление, содержат детали ошибок проверки достоверности модели. Эти детали автоматически обнаруживаются и применяются дескрипторным вспомо­ гательным классом, который используется для трансформации элементов input. Чтобы посмотреть , как все работает, запустите приложение и щелкните на кнопке Make Bookiпg, не заполняя форму данными о встрече. Визуальных изменений в окне браузера не произойдет, но если просмотреть НТМL-разметку, которую МVС возвра­ щает из запроса POST. то вы увидите. что изменился атрибут class элемента формы. Вот как выглядел элемент ClientName до отправки формы: <input class= " form-control " type="text" id="ClientName" name="ClientName" value=" " > А после отправки пустой формы этот элемент input принимает следующий вид: <input class= " form-control input-validation-error" type= " text " id="ClientName" name="ClientName" value=" " > Дескрипторный вспомогательный класс добавляет элементы, чьи значения не прошли проверку достоверности. в класс input-validation-error, который затем можн о стилизовать с целью выделения проблемы. Это можно сделать за счет определения специальных стилей CSS в таблице сти­ лей. но если нужно задействовать встроенные стили для проверки, предоставляемые СSS-библиотеками вроде Bootstrap, придется выполнить небольшую дополнительную работу. Поскольку имя класса. добавляемое к элементам формы. изменять нельзя. потребуется код JavaScript для сопоставления имени. применяемого инфраструкту­ рой МVС, и классами ошибок CSS, предлагаемыми библиотекой Bootstrap. Совет. Использовать код JavaScript подобного рода может быть неудобно, из-за чего возни­ кает соблазн применять специальные стили CSS даже при работе с СSS-библиотеками типа Bootstrap . Однако цвета, используемые классами проверки достоверности в Bootstrap , можно переопределять посредством тем или путем настройки пакета и опре­ деления собственных стилей. Это означает, что вам придется обеспечить соответствие между любыми изменениями в теме и изменениями в любых специальных стилях, которые вы определили. В идеальном случае разработчики из Microsoft сделают имена классов проверки достоверности конфигурируемыми в будущем выпуске ASP.NET Соге MVC , но до тех пор написание кода JavaScript для применения стилей Bootstrap является более надежным подходом, чем создание специальных таблиц стилей. В листинге 27. 11 к представлению Ма keBoo king добавляется код jQuery для по­ ис1ш элементов в классе input-validation-error, нахождения ближайшего роди­ тельского элемента, который был назначен классу form - group, и добавления этого элемента в класс has-error (используемый библиотекой Bootstrap для установки цвета ошибки в элементах формы).
Глава 27 . Проверка достоверности моделей 839 Листинг 27.11 . Назначение элементов классам проверки достоверности в файле MakeBooking. cshtml @model Appointment @{ Layout = "_Layou t"; } @section scripts { <script asp-src -include="/lib/jquery/dist/*.min.js"></script> <script type="text/javascript"> $(document) .ready(function () { $("input.input-validation-error") . closest( ".form-group") .addClass("has-error"); }); </script> <div class= " bg -p rimary panel - body " ><h2>Book an Appointment</h2></div> <form class= " panel - body " asp - action="M akeBooking " method=" post " > <div class= " form - group " > <label asp - for= " Cl i entName " >Your name : </labe l> <input asp - for ="ClientName " clas s="form-con tro l" /> </div> <div class= " form -group " > <label asp - for= "D ate " >Appointment Dat e : < /label> <input asp -for= " Date " type= " text " asp-format=" {О : d} " class= " form - control " /> </div> <di v class=" radio form- gr oup"> <input asp - for= " TermsAccepted " /> <label asp - for= " Term s Accepted " >I accept the terms & conditions</ label> </div> <button type= " submit " class= " Ьtn Ьtn - primary">Make Booking</button> </form> Добавленный код jQuery выполняется, когд а браузер завершает разбор всех эле­ ментов в НТМL-документе, и результатом будет выделение элементов input , которые назначены классу input -val idaton - error . Запустив приложение и отправив форму без заполнения всех полей, можно получить результат, представленный на рис. 27.2 . Когда форм а о тп равляется без ввода каких-либо данных, все три свойства выделяются как содержащие ошибки. Пользов атель не увидит представление Completed . cshtml до тех пор, пока форма не будет отправлена с данными, которые могут быть разобраны связывателем модели и успешно проходят явные проверки до­ стоверности в методе MakeBooking (). Пока это н е произойдет, отправка формы будет приводить к визуал изации представления MakeBooking . cshtml с текущими ошиб­ ками проверки достоверности. Отображение сообщений об ошибках проверки достоверности Классы CSS, которые дескрипторные вспомогательные классы применяют 1~ элементам input, указывают на наличие проблемы с полем формы , но не сообща ­ ют пользователю, в чем конкретно состоит проблем а. Предо с тавл ени е пользоват е­ лю дополнительной информации требует использования другого дескрипторного вспомогательного класса, который добавляет в представление сводку по проблемам (ли стинг 27.12).
840 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC б Modtl Volidotion Х ~- _:_ ?L~-105·;~~~-~~/н~~~:~4~~;8~=~~--~=-°':~:::~=~~:=~--=~=--==-:~=-С-С ~) 1 -1 Yournam e: х 1 1 1 i1 ! 1 i г ·- 1 Appointment Date: i 1 1 О 1accept the terms & coпditions i E oos xj 1 '1'if1Мid 1 1 L______ ·---·-·-----------·--"-----·---·-----~-·-"--------------·------·-------.! Рис. 27.2. Подсвечивание ошибок проверки достоверности Листинг 27. 12. Отображение сводки по проверке достоверности в файле мakeBooking. cshtml @model Appointment @{ Layout = " Layout "; @section scripts { <script asp - src - include= "/l iЬ/jq uer y/dist/* .m in . js " ></script> <script type= " text/ j avascript " > $(document) . ready(funct i on () { $( "input . input - validation-error " ) . closest (". form - group ") . addClass ( "has - error " ) ; }); </script> <div class= " bg-primary panel-body"><h2>Book an Appointment</h2></div> <form class="panel-body" asp-action= "M akeBooking " method=" post " > <div asp-validation-summary="All" class="text-danger"></div> <div class= " form-group " > <label asp - for= " ClientName " >Yo ur n ame : </ label> <input asp - for= " ClientName " class= " form - control " /> </div> <div class="form-group"> <labe l asp - for= " Date " >Appoin tmen t Date:</label> <i nput asp-for= " Date " type= " text " asp- format= " {O : d }" class= " form-control " /> </div> <div c l ass= " radio form-group" > <input asp - for= "T ermsAccept ed " /> <label asp - for= "T ermsAccepted " >I accept the terms & conditions</label > </div> <button type= " submit " class= " Ьtn Ьtn - primary " >Make Booking</b utton > </form>
Глава 27 . Проверка достоверности моделей 841 Класс Validat ionSurnmaryТagHelper обнаруживает атрибуг asp - validation-surnmary в элементах div и реагирует добавлением сообщений, которые описывают любые ошибки проверки достоверности, выявленные методом действия. В атрибуте asp-validation- sumrnary указывается одно из значений перечисл ения ValidationSumrnary, которые описаны в табл . 27.5 и вскоре будуг продемонстрированы. Таблица 27.5 . Значения перечисления ValidationSummary Имя All Описание Это значение применяется для отображения всех ошибок проверки до­ стоверности, которые были зарегистрированы ModelOnly Это значение используется для отображения только ошибок проверки достоверности, относящихся ко всей модели, за исключением тех, кото ­ рые были зарегистрированы для индивидуальных свойств, как описано в разделе " Отображение сообщений об ошибках проверки достовер­ ности на уровне модели" далее в главе None Это значение применяется для отключения дескрипторного вспомога­ тельного класса, так что он не будет трансформировать НТМL-элемент Если вы запустите приложение и отправите форму, не внося какие-либо изме­ нения, то увидите сводку, которую сгенерировал дескрипторный вспомогательный класс. Цвет текста в рассматриваемом примере определяется классом text-danger из Bootstrap, который гарантирует, что цвет текста соответствует цвету, используемо­ му для выделения текстовых полей (рис. 27.3). [j Model V•liclation Х 1~ с [~i;;i1.~~~~}soi~~~~~~~~-~;~~~~--=~=-~~~~~~~~:~~:~------=-~~:~==~--=-~~l__ i 1 1 l j Please enler your name 1 1 • P lease en!er а date in !he futuгe j • You mus t accept the terms 1 J Your nam e: 1 1 c-----------------------XJ 1 1 1 • Appointmenl Date : ! 1 ~"'"°'___ ~1 i 1 1 О 1accept the terms & conditlons , diiiilr'·I 1 [_____" _ _ ________ ________ ______ .__ __ " __ --"---·-· ---·---·-"--- --"--------·---·----·-·------"-·-----"-) Рис. 27.З. Отображение пользователю сводки по проверке достоверности
842 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Заглянув в НТМL-разметку, которая бьmа получена браузером , вы заметите , что сообщения проверки достоверности отправлялись в виде списка: <div c l ass= " text - danger validation - summary - errors " data-valmsg - summary= " true " > <ul > <l i >Please enter your name</li> <li> Pleas e enter а date in the future</li> <li>You must accept the te rms </li> </ul> </div> Конфигурирование стандартных сообщений об ошибках проверки достоверности Процесс привязки моделей, описанный в главе 26, выполняет собств енную провер­ ку достоверности, когда он пытается предоставить значения данных , требующиеся для вызова метода действия. Чтобы увидеть, как это работает, запустите приложение , очистите содержимое поля Appoiпtmeпt Date (Дата встречи) и отправьте форму . Бы обнаружите, что одно из отображаемых сообщений проверки достоверности измени­ лось, и его нет среди строк, передаваемых методу AddModelError () внутри метода действия: The value ' ' is invali'd Показанное сообщение добавляется в ModelStateDictionary процессом привяз­ ки моделей, когда он не может найти значение для свойства или находит его, но не может преобразовать. В данном случае ошибка возникла из -за отправки в данных формы пустой строки , которая не может быть преобразована в объект Da teT ime дл я свойства Date 1<.ласса Appointment . Связыватель модели располагает набором предопределенных сообщений , которые он применяет для обозначения ошибок пров ерки достоверности. Их можно заменить специальными сообщениями за счет присваивания функций свойствам, определен­ ным в интерфейсе IModelBindingMes sageProvider (табл. 27.6). Таблица 27.б. Свойства интерфейса IModelBindingMessageProvider Имя ValueMustNotBeNullAccessor MissingBindRequiredVa l ueAccessor MissingKeyOrValueAccessor Описание Функция , присвоенная этому свойству, исполь­ зуется для генерации сообщения об ошибке проверки достоверности, когда свойству моде­ ли, не допускающему null , поступает значе­ ние nul l Функция , присвоенная этому свойству, приме ­ няется для генерации сообщения об ошибке проверки достоверности, когда запрос не со­ держит значение для обязательного свойства Функция, присвоенная этому свойству, исполь­ зуется для генерации сообщения об ошиб ке проверки достоверности, к огда данные , требу ­ ющиеся объекту словаря, содер жат ключи или значения, равные null
Имя AttemptedValueisinvalidAccessor UnknownValue i sinvali dAccessor ValueMustBeANumЬerAccesso r ValueisinvalidAccessor Глава 27. Провер к а достоверности моделей 843 Окончание табл. 27.6 Описание Фун кция, присвоенная этому свойству, приме­ няется для генерации сообщения об ошибке проверки достоверности, когда системе при­ вязки моделей не удается преобразовать зна­ чение данны х в требуемый тип С# Функция, присвоенная этому свойству, исполь­ зуется для генерации сообщения об ошибке проверки достоверности , когда системе при­ вязки моделей не удается преобразовать зна­ чение данны х в требуемый тип С# Функция, присвоенная этому свойству, применя­ ется для генерации сообщения об ошиб ке про­ верки достоверности, когда значение данных не может быть преобразовано в числовой тип С# Функция, присвоенная этому свойству, исполь­ зуется для генераци и запасного сообщения об ошибке провер ки достоверности, которое при ­ меняется в качестве последнего средства Все функции. присваиваемые свойствам из табл. 27.6, получают стр01~у. содержа­ щую значение данных из запроса, и возвращают строку с сообщением об ошибке. В классе Startup специ ал ьные функции можно сконфигуриров ать как п а р аметры, что проиллюстрировано в листинге 27.1 3, где производится замена стандартной фун кции ValueMustNotBeNullAccessor. Листинг 27 .1 З. Замена функци и генерации сообщений при привязке моделей в файле Startup. cs using Microsoft . AspNetCore . Builder ; using Microsoft . Extensions. Depende n cylnject i on ; namespace ModelVa l idation puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services .Adc!Мvc () .AddМvcOptions (opts => { opts . ModelBindingMessageProvider . ValueMustNotBeNullAccessor = value => "Please enter а value"; }); puЫic void Configure ( IAp p licat i onBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app.UseMvcWithDefaultRoute() ;
844 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Свойство MvcOptions. ModelBindingMessageProvider возвращает объект ModelBindingMessageProvider, который реализует интерфейс IModelBinding MessageProvider и может использоваться для замены стандартных функций гене­ рации сообщений. В листинге 27. 13 определена функция, возвращающая сообщение Please enter а value (Введите значение); чтобы увидеть результат, запустите прило­ жение и отправьте форму. предварительно очистив поле Appointment Date (рис. 27.4). ~ M<>del Volidotion ~ С Г(i)l~s750/Home/МakeBoo;;;;;-;;-------------···-- 1 • Please enter you r name • Please enter а value 1 • Yo u must accept the terms Yourna me: j J:::;;~ Рис. 27.4 . Эффект от изменения функции генерации сообщений при привязке моделей Отображение сообщений об ошибках проверки достоверности на уровне свойств Несмотря на то что специальное сообщение об ошибке более выразительно, чем стандартное сообщение, польза от него по-прежнему невелика, т.к. оно не указыва­ ет ясно пользователю на проблему. Для ошибок такого рода более практично отоб­ ражать сообщения об ошибках проверки достоверности рядом с НТМL-элементами. которые содержат проблемные данные. Это можно делать с применением дескрип­ торного вспомогательного класса ValidationMessageTag. Он ищет элементы span с атрибутом asp-validation-for, используемым для указания свойства модели, к которому должны относиться отображаемые сообщения об ошибках. В листинге 27 .14 для каждого элемента input внутри формы добавляются элементы сообщений об ошибках проверки достоверности на уровне свойств. Индивидуальные сообщения об ошибках проверки достоверности обеспечат достаточное выделение для того, чтобы можно было понять, какие элементы содержат ошибки. Листинг 27 .14 . Добавление сообщений об ошибках проверки достоверности на уровне свойств в файле MakeBooking. cshtml @model Appointment @{ Layout = " Layout"; @section scripts { <scri pt asp-src-include="/liЬ/jquery/dist/*.min.js"></script> <script type="text/javascript">
Глава 27 . Проверка достоверности моделей 845 $(document) . ready(function () { $( "i nput . input -validation -e rror " ) . closest ( ". form - group " ) . addClass ( "has-error"); }); </script> <div class= " bg - primary panel-body"><h2>Book an Appointment</h2></div> <form class= " panel - body " asp - action= "M akeBooking " method= " post "> <div asp - validation - summary=" All " class =" text - danger " ></div> <div class= " form - group " > <label asp-for= " ClientName " >Your name: </labe l> <divXspan asp-validation-for="ClientName" class="text-danger"X/span> </div> <inp ut asp-for= " ClientName " class= " form - control " /> </div> <div class= " form - group " > < label asp - for= " Date " >Appointment Date : </labe l> <divXspan asp-validation-for="Date" class="text-danger"></span> </div> <input asp - for= "Date" type= " text" asp-foпnat=" {О: d} " class= " form - con t rol" /> </div> <span asp-validation-for="TermsAccepted" class="text-danger"></span> <div class= " radio form - group " > <inp ut asp - for= " TermsAccepted " /> <label asp-for= "T ermsAccepted " >I accept t he terms & conditions</label> </div> <butto n type= " submit " class= " Ьtn Ьtn-primary">Make Booking</button> </form> Поскольку элементы span отображаются внутри , нужно позаботиться о том, что­ бы было вполне очевидно, к каким элементам относятся сообщения об ошибках про­ вер1ш достоверности. Запустив приложение и отправив форму без предварительного ввода данных, можно увидеть новые сообщения об ошибках проверки достоверности (рис. 27.5). Отображение сообщений об ошибках проверки достоверности на уровне модели Может п01щзаться, что сводка по проверке достоверности в приложении избыточ­ на, т. к. она просто .цублирует сообщения уровня свойств, которые в целом более полез­ ны пользователю , поскольку они находятся рядом с элементами формы, где должны быть устранены проблемы. Но сводка позволяет предпринять удобный трюк, который заключается в возможности отображать сообщения, применимые ко всей модели, а не только к отдельным свойствам. Другими словами, можно сообщать об ошибках, ко­ торые возникают в комбинации индивидуальных свойств, например, когда заданная дата допустима только в сочетании со специфическим именем . В листинге 27 . 15 добавлена проверка достов ерности, которая пр едотвращает на­ значение встр ечи пользователем по имени Джо (Joe) по понедельникам (M onday).
846 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC • Please enter your name • Please ente r а value • You must accept th e terms Youгname: Please енtег уоuг name Appointment Date: Please ente r а value 1 ~us t accept tt1e terms ~·=~'""'~'&""~~--------··-------------------__] Рис. 27.5. Применение сообщений об ошибках проверки достоверности на уровне свойств Листинг 27.15. Выполнение проверки на уровне модели в файле HomeController. cs using System; using Microsoft . AspNetCore.Mvc ; using ModelValidation.Models ; using Microsoft.AspNetCore.Mvc . ModelBinding ; namespace ModelValidation . Controllers { puЫic class HomeController : Controller puЫic IActionResult Index() => View ( " MakeBooking ", new Appointment () [HttpPostJ Date = DateTime.Now )) ; puЫic ViewResult MakeBooking(Appointment appt) if (string.IsNullOrEmpty(appt . Cl ientName)) { ModelState . AddMode l Error(nameof(appt.C lientName), " Please enter your name " ) ; if (ModelState . GetValidationState( " Date " ) == ModelValidationState . Valid && DateTime.Now > appt .D ate) { ModelState.AddModelError(nameof(appt.Date) , "Please enter а date in the future") ;
Глава 27. Пров е р ка достоверности моделей 847 if (!appt.TermsAccepted) { ModelState . AddModelError(nameof(appt . TermsAccepted), " You must accept the terms" ) ; if (ModelState.GetValidationState(nameof(appt.Date)) == ModelValidationState.Valid && ModelState.GetValidationState(nameof(appt.ClientName)) == ModelValidationState.Valid && appt . ClientName. Equals (" Joe" , StringComparison . OrdinalignoreCase) && appt . Date . DayOfWeek == DayOfWeek .Monday) { ModelState.AddМodelError("", "Joe cannot book appointments on Mondays"); if (ModelState . IsValid) { return View ( " Completed ", appt) ; else { return View () ; Код выглядит более запутанным. чем есть на самом деле , что отража ет саму при­ роду проверки достоверности данных. Допустимо сть значений ClientName и Date проверя ется путем инспектирования состояния модели перед выяснением, выпада ­ ет ли указанная дата на понедельник и содержит ли свойство ClientName строку Joe. Если Джо пытается назначить встречу на понедельник, тогда вызывается метод AddModelError () с передачей в первом аргументе пустой строки(""), которая ука­ зывает на то, что ошибка касается всей модели, а не отдельного свойства. В листинге 27.16 значение атрибута asp - validation -s ummary изменено на Mode l On l y , что приводит к исключению ошибок уровня свойств, поэтому сводка будет отображать только сообщения об ошибках , которые применимы к модели целиком. Листинг 27 .16. Отображение сообщений об ошибках проверки достоверности на уровне модели в файле MakeBooking. cshtml @mode l Appo intment @{ Layout = " _ Layout "; @section scripts { <script asp - src - inc lude = " /liЬ/jquer y/di st/* . min . js " ></script> <script type= " text/javascript " > $(doc ume nt) . ready(funct i on () { $("input . input- validation- error " ) . closest(" .form-group " ) . addCl ass( " has - error " ) ; }); </script> <div class= " bg - primary panel - body " ><h2>Book ап Appointment</h2></ d iv> <form class= " panel - body " asp - action= " MakeBooking " method= " post " > <div asp-validation-sununary="ModelOnly" class="text-danger"></div>
848 Часть 11. Подробные сведения об инфраструктуре ASP. NET Соге MVC <div class= "form- group " > <label asp -for= " ClientName " >Your name : </ label> <div><span asp-validation-for="ClientName" class= " text - danger " ></span> </div> <inp ut asp-for= " ClientName " c l ass= " form - control" /> </div> <div cl ass=" form-group"> <label asp - for= " Date">Appointment Date : </label> <d iv> <span asp-validation- for= " Date " class="text-danger " ></span></div> <input asp- for= 11 Date 11 type= 11 text 11 asp- format= 11 {О : d} 11 class= 11 form - control 11 /> </div> <s pan asp - va lidation-for= 11 TermsAcce pted" c l ass= " text -danger " ></span> <d iv class= " radio form- group " > <inp ut asp -for= "T ermsAccepted " /> <la bel asp - for= " TermsAccepted " >I accept the terms & conditions</label> </div> <but ton type= " submit 11 class= " Ьtn Ьtn-primary">Make Booking</button> </fo rm> Запустите приложение , введите Joe в поле ClientNarne и выберите дату, выпада­ ющую на понедельник , такую как 18 января 2027 года (01/18/2027). Отправив фор­ му, вы получите ответ, показанный на рис. 27.6 . • Joe cannot book appoir1tments on Mo11days Yourname : ' Joe Appointment Date : 01 / 18/2027 You must accept the terms О 1асс ер11!1е terms & conditions <'~ ~ ' 1 _,.· M~ke _ ~~ki~g -~ L..•·---·------·------·----·-··-·--·-··- ·---·····-· --·-····--·"-·--···-·---·-···--·-··---·-··----·-···-·----- -·-"-·--·J Рис. 27.6. Использование сообщений об ошибках проверки достоверности на уровнях модели и свойств
Глава 27 . Проверка достоверности моделей 849 Указание правил проверки достоверности с помощью метаданных Одна из проблем с помещением логики проверки достоверности внутрь мето­ да действия связана с тем, что в итоге она дублируется в каждом методе действия, который получает данные от пользователя. Чтобы помочь сократить дублирование, процесс проверки достоверности поддерживает использование атрибутов. Атрибуты позволяют выражать правила проверки достоверности моделей прямо в классе моде­ ли, гарантируя применение одного и того же набора правил независимо от метода, который используется для обработки запроса. В листинге 27. 17 к классу Арро intment применяются атрибуты для навязывания того же набора правил проверки достоверности на уровне свойств, который исполь­ зовался в предыдущем разделе. Листинг 27 .17 . Применение атрибутов проверки достоверности в файле Appointment. cs using System ; using System . ComponentModel .D ataAnnotat i ons ; namespace ModelValidation . Models puЫic class Appointment { [Required] [Display (Name = "name")] puЬlic string Client Name { get; set; } [UIHint ("Date" )] [Required (ErrorMessage = "Please enter а date")] puЬlic DateTime Date { get ; set ; } [Range(typeof(Ьool), "true", "true", ErrorMessage = "You must accept the terms")] puЫic bool TermsAccepted { get ; set ; } Здесь используются два атрибута проверки достоверности - Required и Range . Атрибут Required указывает, что ошиб1ш проверки достоверности возникает, если пользователь не отправил значение для свойства . Атрибут Range задает подмножес­ тво приемлемых значений. В табл. 27.7 приведен набор встро енных атрибутов про­ верки достоверности, доступных в приложении МVС. Все атрибуты проверки достоверности позволяют указывать специальное сообще­ ние об ошибке за счет установки значения для свойства ErrorMessage. например: [UIHint("Date " ) ] [Required(ErrorMessage = "Please enter а date")] puЬlic DateTime Date { get ; set ; }
850 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Таблица 27. 7 . Встроенные атрибуты проверки достоверности Атрибут Compare Range RegularExpression Required StringLength Пример [Compare ( "ДругоеСвойство " ) ] [Range (1О, 2О) ] [RegularExpression ( "шаблон" )] [Required] [StringLength(lO)] Описание Этот атрибут гарантирует, что свойства имеют одно и то же значение . Это по­ лезно, когда вы предлагаете пользова­ телю два раза предоставить ту же самую информацию, такую как адрес эле ктрон­ ной почты или пароль Этот атрибут гарантирует, что число­ вое значение (или значение свойства любого типа, реализующего интер­ фейс IComparaЫe ) не находится за рамками заданны х минимального и максимального значений. Чтобы ука­ зать границу только с одной стороны , применяйте константу MinValue или MaxValue (например, [Range (int. MinValue , 50)] ) Этот атрибут гарантирует, что строко­ вое значение соответствует указанно­ му шаблону регулярного выражения. Обратите внимание , что шаблон должен соответствовать всему предостав­ ленному пользователем значению, а не только подстроке внутри него. По умолчанию при сопоставлении учитыва­ ется регистр символов, но с помощью модификатора (? i ) его можно сделать нечувствительным к регистру, напри­ мер, [RegularExpression( " (?i) шаблон")] Этот атрибут гарантирует, что значение не является пустой строкой или строкой , состоящей только из пробелов. Чтобы трактовать пробельные символы как допустимые, необходимо использовать [Required(AllowEmptyStrings= true)] Этот атрибут гарантирует, что строковое значение не превыша­ ет указанную максимальную длину. Можно также задавать минималь­ ную длину : [ StringLength (10 , MinimumLength=2)]
Глава 27. Проверка достоверности моделей 851 Если специальное сообщение об ошибке не указано, тогда будут применяться стан­ дартные сообщения, но они имеют тенденцию раскрывать детали класса модели, ко­ торые не имеют смысла для пользователя, если только также не используется атрибут Display, как делалось в отношении свойства ClientNarne: [Required] [Display (Narne = "narne")] puЫic string ClientName { get; set ; } Стандартное сообщение, генерируемое атрибутом Required, отражает имя, ука­ занное с помощью атрибута Display, поэтому оно не раскрывает пользователю имя самого свойства . Для обеспечения согласованной работы проверки достоверности такого рода тре­ буется определенное внимание. В качестве примера взгляните на атрибут проверки достоверности, примененный к свойству TermsAccepted: [Range(typeof(bool), "true", "true", ErrorMessage="You must accept the terms")] puЫic bool TermsA ccep ted { get ; set; } Необходимо удостовериться, что пользователь отметил флажок для принятия ус­ ловий. Использовать атрибут Required нельзя, т.к. брауз ер отправит для данного свойства значение false, если пользователь не отметил флажок. Проблема решае тся за счет возможности атрибута Range предоставлять объект Туре и указывать верх­ нюю и нижнюю границы в виде строковых значений. Установка обеих границ в true создает эквивалент атрибута Required для свойств типа bool, которые редактиру­ ются с применением флажков. Обеспеч ение успешной совместной работы атрибутов проверки достоверности и данных, отправляемых браузером , может потребовать не­ которого экспериментирования. Использование атрибутов проверки достоверности в классе модели означает, что метод действия в контроллере можно упростить, как пока зан о в листинге 27 .18. Листинг 27 .18. Удаление проверки достоверности на уровне свойств в файле HomeController. cs using System; using Microsoft . AspNetCo re.Mvc ; using ModelValidation.Models; using Microsoft . AspNetCore . Mvc . ModelBinding ; namespace ModelValidation . Controllers { puЫic class HomeControl ler : Controller puЫic IActionResult Inde x() => View( "MakeBooking ", new Appointment() [HttpPost] Date = DateTime .N ow }) ; puЫic ViewResult MakeBooking(Appointment appt) if (ModelState.GetValidationState(nameof(appt.Date)) == ModelValidationState.Valid && ModelState.GetValidationState(nameof(appt.ClientName)) == ModelValidationState.Valid
852 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC && appt.ClientName.Equals("Joe", StringCornparison.OrdinalignoreCase) && appt.Date . DayOfWeek == DayOfWeek.Monday) { ModelState.AddМodelError("", "Joe cannot book appointrnents on Mondays 11 ); if (ModelState.IsValid) { return View ( 11 Completed11 , appt) ; else { return View(); Атрибуты проверки достоверности применяются перед вызовом метода действия, т.е. при выполнении проверки достоверности на уровне модели для выяснения, до­ пустимы ли индивидуальные свойства , можно по-прежнему полагаться на состояние модели. Чтобы взглянуть на атрибуты проверки достоверности в действии, запустите приложение и отправьте форму, не вводя какие-либо данные (рис. 27.7). D MO<i•IVa lidatio n х г;;--·-·-·- ---------·---------- -- ---·---··- ----- ---· i С Lф locall1ost:55750/liome/M.' !ke8ook111g -i:tj ; 1 --~~. - · - ·-- .~-- ·--·-·· ~- -·--::.-~.--==-::.._--:-· ~-::-::.:::::-·:-_.-;;-:-~ - - · ·· Your name : The name field ls required. App oin lm enl Date : Please enter а value YoL1 musl accep t lhe lerms О 1accepl tl1e term s & condit ions 1 1 i1 1 ! '1 1 *Иi=iid 1 -·----------·-·---- --- --------------- -·--- -- -·- ___ J Рис. 27. 7. Использование атрибутов проверки достоверности Создание специального атрибута проверки достоверности для свойства Процесс проверки достоверности может быть расширен за счет создания атрибута , который реализует интерфейс IModelValidator. Создайте папку Infrastructure и добавьте в нее файл класса по имени MustBeTrueAt tribute. cs с определением, приведенным в листинге 27.19.
Глава 27. Проверка достоверности моделей 853 Листинг 27.19. Содержи мое файла MustBeTrueAttribute. cs из папки Infrastructure using System ; using System . Collections . Gener i c ; using System . Linq ; using Microsoft . AspNetCore.Mvc . ModelBinding . Val i dation ; namespace ModelValidation . Infrastructure { puЫic class MustBeTrueAttribute : Attrib ute , I Mode l Validator { puЫic bool IsRequired => true ; puЬlic string ErrorMessage { get ; se t; ) = " This value must Ье true "; puЫic I EnumeraЫe<ModelValidationRes u lt> Val i date( ModelValidationContext context) { bool? value = context . Model as bool? ; if (!val ue.HasValue 11 val ue.Val ue == false) return new List<ModelVal i dationRes u lt> { new ModelValidat i onRe s ult( "", Er r orMessage) ); else { r eturn Enume raЫe . Empty<ModelValidationResult>() ; Б интерф ейсе IModelValidator определено свойство I sRequ ired, которое приме­ няет ся для ук азан ия, тр е бу ется ли проверка достоверности с помощью этого класса (что немного вводит в заблуждение , т.к . значение, возвращаемое данным свойством , просто испол ьзуется для упорядочения атрибутов проверки достоверности. чтобы обяз ательны е атрибуты выполнялись первыми). Метод Validate () применяется для выполн ен ия проверки достоверности и получает информацию чер ез экз е мпляр клас са ModelValidationContext . наиболее полезные сво й ства ко торого описаны в табл . 27.8. Таблица 27 .8 . Полезные свойства класса ModelValidationCon text Имя Mode l Contai ner ActionContext ModelMetadata Описание Это свойство возвращает значение свойства, подлежащего проверке достоверности, которым в рассматриваемом примере будет значение свойства Terms Accepted Это свойство возвращает содер жащий свойство объект, которым в рассматриваемом примере будет объект Appointment Это свойство возвращает объект Act i o n Context , предоставляющий данные контекста и описывающий метод действия, который будет об­ рабатывать запрос Это свойство возвращает объект Mode lMetad ata , который детально описывает класс модели, подвергающийся проверке достоверности
854 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Метод Validate () возвращает последовательность объектов Model ValidationResult, каждый из которых описывает одиночную ошибку проверки достоверности. В примере атрибута объект ModelValidationResul t создается, если значение context. Model не равно true. В первом аргументе конструктору ModelValidationResult переда­ ется имя свойства, с которым ассоциирована ошибка, что указывается как пустая строка при проверке достоверности индивидуальных свойств . Во втором аргументе задается сообщении об ошибке, которое будет отображаться пользователю. В листин­ ге 27.20 атрибут Range заменен специальным атрибутом . Листинг 27.20. Применение специального атрибута в файле Appointment. cs using System; using System .ComponentModel .DataAnnotati ons ; using ModelValidation.Infrastructure; n a mespace ModelValidation.Models puЬlic class Appointment { [Required] [Display(Name = "name")] puЫic string ClientName { get; set; } [UIHint("Date")] [Required(ErrorMessage = "Please enter а date")] puЫic DateTime Date { get; set; } [MustBeTrue (Erro rMessage = "You must accept the terms")] puЫic bool TermsAccepted { get; set; } Результат использования специального атрибута проверки достоверности будет точно таким же, как и атрибута Range, но при чтении кода цель специального атри ­ бута более очевидна. Выполнение проверки достоверности на стороне клиента Все продемонстрированные до сих пор приемы проверки были примерами провер­ ки достоверности на стороне сервера. Это означает, что пользователь посылает свои данные серверу, сервер проверяет данные и отправляет обратно результаты проверки (либо признак успешности, либо список ошибок, подлежащих исправлению) . В веб-приложениях пользователи обычно ожидают немедленного отнлика провер­ ки достоверности, без необходимости отправки чего-либо серверу. Такая возможность известна нан проверка достоверности на стороне клиента и реализуется с примене­ нием JavaScript. Вводимые пользователем данные проверяются на предмет достовер­ ности перед их отправкой серверу, снабжая пользователя немедленным откликом и шансом скорректировать любые проблемы . Инфраструктура МVС поддерживает ненавязчивую проверку достоверности на сто­ роне клиента. Термин ''ненавязчивая" означает, что правила проверки достоверности выражаются с использованием атрибутов, которые добавляются к НТМL-элементам, ге­ нерируемым представлениями . Атрибуты интерпретируются библиотекой JavaScript, входящей в состав инфраструктуры МVС, которая, в свою очередь, конфигурирует биб-
Глава 27. Проверка достоверности моделей 855 лиотеку jQuery Validation , выполняющую действительную работу по проверке достов ер­ ности . В последующих разделах будет показ ано, как работает встроенная поддержка проверки достоверности, и продемонстрированы способы расширения ее функцио­ нал ьности для об ес печения проверки достов ерности на стороне клиента. Совет. Проверка достоверности на стороне клиента сосредоточена на проверке достовер­ ности индивидуальных свойств. В действительности настроить проверку достоверности клиентской стороны на уровне модели с помощью встроенной в MVC поддержки нелегко . С этой целью в большинстве приложений MVC проверка на стороне клиента применяется для свойств, а проверка на стороне сервера - для всей модели. Прежде всего , понадобится добавить в приложение новые пакеты JavaScript, ис­ пользуя Bower (листинг 27.21). Листинг 27. 21 . Добавление пакетов в файле bower . j son " name ": " asp.net", " private ": true , " dependencies ": { "bootstrap": "3.3 .6", " jquery": "2 .2 .4", "j query-validation" : "1. 15. О" , "jquery-validation-unoЬtrusive": 11 3. 2 . 5" Применение проверки достоверности на стороне клиента означает добавление к представлению трех файлов JavaScript: библиотеки jQuery, библиотеки проверки до­ стоверности jQuery и библиотеки ненавязчивой проверки достоверности от Microsoft, которы е все можно видеть в листинге 27.22 . Листинг 27 .22. Добавление элементов проверки достоверности JavaScript в файле мakeBooking. cshtml @model Appointment @{ Layout = " Layout "; @section scripts { <script asp-src-include="lib/jquery/dist/ * .min.js"></script> <script asp - src-include="liЬ/jquery-validation/dist/jquery . *.min.js"> </script> <script asp-src-include="liЬ/jquery-validation-unoЬtrusive/*.min.js"> </script> <div class =" bg-primary panel - body" ><h2>Book an Appointment</h2></di v > <form clas s = " panel-body" a sp-action= " MakeBooking " method= " post " > <div asp- validati on- summa r y= " ModelOnly" class=" text- danger "></div> <div class= " form- group"> <label asp- for=" Client Name " >You r name : </label > <div>< s pan asp -validation- fo r = "ClientName" clas s = " text - da ng er " ></sp an> </div>
856 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC <input asp -for= " ClientName " class= " form - control " /> </div> <div class= " form- group " > <label asp -for= "D ate " >Appointment Date : </ label> <div><span asp-validation-for= " Date " class= " text -danger " ></s pan> </div> <input asp-for= " Date " type= " text " asp- format= " {О : d}" class=" form-control" /> </div> <span asp - validation-for= "T ermsAccepted " class= " text - danger " ></span> <div class= " radio form- group " > <input asp - for= "T ermsAccepted " /> <label asp - for= "T ermsAccepted" >I accept the terms & conditions</label> </div> <button type= " submit " class= " Ьtn Ьtn-p r imary">Make Booking</button> </fo rm > Файлы должны добавляться в показанном порядке. Когда дескрипторные вспо­ могательные классы трансформируют элементы input, они инспектируют атрибуты проверки достоверности, примененные к свойствам класса модели, и добавляют ат­ рибуты к выходным элементам. Запустив приложение и просмотрев НТМL-разметку, отправленную браузеру, вы заметите элемент следующего вида: <input class= " form - control " type= " text " data-val="true" data-val-required="The name field is required." id= " ClientName " name= " Cl i entName" value= "" /> Код JavaScript ищет элементы с атрибутом data - val и выполняет локальную про­ верку достоверности в браузере. когда пользователь отправляет форму. не посылая НТГР-запрос серверу. Чтобы увидеть эффект, запустите приложение и отправьте фор­ му, одновременно используя инструменты <F12> для наблюдения за тем, что сообще­ ния об ошибках проверки достоверности отображаются, хотя никакие НТГР-запросы серверу не отправлялись. Избегание конфликтов с проверкой достоверности, встроенной в браузеры Ряд браузеров текущего поколения, поддерживающих HTML5, обеспечивают простую про­ верку достоверности на стороне клиента, которая основана на атрибутах, применяемых к элементам input . Общая идея в том, что элемент input, к которому применен атрибут required , например, вызовет отображение браузером сообщения об ошибке проверки достоверности, если пользователь попытается отправить форму, не предоставив значение для этого элемента. В случае генерации элементов форм из моделей никаких проблем со встроенной в браузеры проверкой достоверности возникать не будет. Дело в том, что для указания правил провер­ ки инфраструктура MVC генерирует и использует атрибуты данных (таким образом, элемент input , который дол жен иметь значение, помечается атрибутом data - val - required, не распознаваемым браузерами). Тем не менее, вы можете столкнуться с проблемами, если не в состоянии полностью кон­ тролировать разметку в приложении, что часто происходит при поступлении содержимо­ го, сгенерированного в другом месте . В результате с формой может работать и проверка достоверности jQuery, и проверка достоверности, встроенная в браузер, что всего лишь сбивает с толку пользователя. Чтобы избежать такой проблемы , к элементу form можно добавить атрибут novalidate.
Глава 27. Проверка достоверности моделей 857 Одна из замечательных характеристик проверки достоверности на стороне клиента МVС связана с тем. что те же самые атрибуты. используемые для указания правил про­ вер1ш достоверности, применяются внутри клиента и на сервере. Это означает. что дан­ ные, поступающие из браузеров , которые не поддерживают JavaScript. подВергаются та­ кой же проверке достоверности. что и данные из браузеров. поддерживающих JavaScript. безо всяких дополнительных усилий. Однако это также означает, что специальные атри­ буты проверки достоверности при проверке на стороне клиента не поддерживаются, по­ тому что код JavaScript не имеет возможности реализовать специальную логику внутри клиента. Иными словами, при желании использовать проверку достоверности на стороне клиента необходимо придерживаться встроенных атрибутов, описанных в табл. 27.7. Сравнение проверки достоверности на стороне клиента в MVC и проверки достоверности с помощью библиотеки jQuery Validatioп Функциональность проверки достоверности на стороне клиента в MVC построена на ос­ нове библиотеки jQuery Validation. Если хотите, то можете работать с библиотекой jQuery Validatioп напрямую, игнорируя средства MVC. Библиотека jQuery Validation обладает высо­ кой гибкостью и большими возможн остями . Ее имеет смысл исследовать хотя бы для того, чтобы понять, каким образом настраивать средства MVC, чтобы извлечь максимальную пользу из доступных вариантов проверки. Библиотека jQuery Validation подробно описана в книге jQuery 2.0 для профессионалов (ИД " Вильяме", 2016 год). Выполнение удаленной проверки достоверности В завершение главы мы рассмотрим удаленную (дистанционную) проверку досто­ верности. Это прием проверки достоверности на стороне клиента, при котором для выполнения проверки вызывается метод действия на сервере. Распространенный пример удаленной проверки достоверности предусматривает выяснение доступности имени пользователя в приложениях, когда оно должно быть уникальным, после чего пользователь посылает данные и производится проверка до­ стоверности на стороне клиента. В качестве части такоrо процесса серверу отправля­ ется запрос Ajax для проверки имени пользователя. Если имя пользователя уже было выдано, тогда отображается сообщение об ошибке проверки и пользователю предо­ ставляется возможность ввести другое имя. Процесс может выглядеть похожим на обычную проверку достоверности на сто­ роне сервера . но данный подход обладает рядом преимуществ. Во-первых. удаленно проверяться будут только некоторые свойства; ко всем остальным значениям данных. которые ввел пользователь, будет по-прежнему применяться проверка достоверности на стороне клиента. Во-вторых, запрос является относительно легковесным и сосре­ доточенным на проверке достоверности, а не на обработке целого объекта модели. Третье отличие связано с тем, что удаленная проверка достоверности выполнятся в фоновом режиме. Пользователю не приходится щелкать на кнопке отправки и ожи­ дать визуализации нового представления. В итоге пользователи получают более от­ зывчивый интерфейс, что особенно важно в случае медленного сетевого соединения между браузером и сервером. Тем не менее, удаленная проверка достоверности сопряжена с компромиссом. Она соблюдает баланс между проверками на стороне клиента и на стороне сервера. но требует отправки запросов серверу приложений, поэтому не будет настолько быст­ рой. как обычная проверка достоверности на стороне клиента.
858 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Первый шаг в сторону использования удаленной проверки достоверности предпо­ лага ет создание метода действия, который мож ет проверить одно и з сво йств мод ел и . Мы будем пров ерять сво йство Date модели Appo intment , чтобы обеспечить нахож­ дени е запрошенной даты встречи в будушем . (Это одно из исходных правил проверки достоверности, применяемых в н ачале главы, но его нево зможно ре ализовать с помо­ щью ст андартных средств проверки н а сторон е клиента.) В листинге 27.23 прив еден код метода действия ValidateDate () ,добавленного в контроллер Home . Листинг 27.23. Добавление метода действия для проверки достоверности в файле HomeController. cs using System ; using Micro s of t. As pNetCore . Mvc ; using ModelVal i dat i on . Models ; using Microso f t . AspNetCore . Mvc . ModelBinding ; name s pace Mode l Val i dation . Control l ers { puЫic class HomeController : Control ler puЫic I ActionResu l t Ind ex () => View ( "MakeBooki ng ", new Appoi ntment () Date = DateTime . Now }) ; [HttpPost] puЫic ViewRes ul t MakeBooking(Ap pointment appt) i f (Mode l State. GetVa lida ti onS t ate(name of(appt . Date)) == ModelVa l idationState . Val i d && Mode l St ate . GetVa l idat i o n State(name of(appt . Cl i entName)) == Mode l ValidationSta t e . Va l id && appt . ClientName . Equ als( " Joe", St ringCompari s on . OrdinalignoreCase) && appt . Da te. DayOfWeek == DayOfWeek . Mond a y) { ModelState . Ad dModel Erro r ( "", " Joe cannot book appoin t ments on Mondays " ) ; i f (ModelState . I sVal id) { ret urn View ("Complet ed", appt) ; else { returnView(); puЬlic JsonResult ValidateDate(string Date) { DateTime parsedDate; if (!DateTime.TryParse(Date, out parsedDate)) { return Json ( "Please enter а valid date (mm/dd/yyyy) ") ; else if (DateTime. Now > parsedDate) { return Json ("Please enter а date in the future"); else { return Json(true); Методы действий, поддержив ающие удаленную проверку достоверно сти, должны возвращать объект типа JsonResul t, который сообщает инфраструктуре MVC о том, что работа производится с данными JSON, как объяснялось в главе 20 . В дополне­ ние к возвращению такого ре зультат а методы действий для проверки достоверности
Глава 27 . Проверка достов е рности моделей 859 должны определять параметр, который имеет то же самое имя , что и проверяемое пол е данных ; в рассм атриваемом примере это Date. Внутри метода действий пров е р­ ка достов ерности вьmолняется путем преобразования значения в объект Da tе т i me и выя снения. отно с ится ли дата к будущему. Совет. Можно было бы воспользоваться привязкой моделей, так что параметром метода дейс­ твия стал бы объект DateTime, но тогда метод проверки достоверности не вызывался бы в ситуации, когда пользователь ввел бессмысленное значение вроде apple . Причина в том, что связывателю модели не удается создать объект DateT i me из app l e и генерируется исключение . Средство удаленной проверки достоверности не способно обработать такое исключение , поэтому исключение молча отбрасывается . В итоге возникает нежелательный эффект: поле данны х не подсвечивается, создавая впечатление , что введенное пользовате­ лем значение является допустимым. Как правило, наилучший подход к удаленной проверке достоверности предусматривает получение методом действия параметра string и явное выполнение любого преобразования типа, разбора или привязки модели . Результаты проверки достоверности выражаю тся с применением метода Json () . создающего результат в формате JSON, который может разобрать и обработать сце­ н арий удаленной проверки достоверности на стороне клиента. Если значение допус ­ тимо. тогда в качестве параметр а методу Js o n () передается t rue : return Json(true); При наличии проблемы в параметре передается сообщение об ошибке проверки. которое должен увидеть пользователь: return Json ( "Please enter а date in the future" ) ; Чтобы использовать метод удаленной проверки достоверности. к нужному свойс­ тву класса модели применяется атрибут Remo t e (листинг 27.24) . Листинг 27 .24 . Использование атрибута Remote в файле Appoin tment. cs using System ; using System . Compone ntModel . DataAnnotat i ons ; using ModelValidation .Infrast r uc tu r e; using Мicrosoft.AspNetCore.Mvc; namespace ModelVa lidat i on . Mo d el s puЫic class Appoint ment { [Requ i red] [Display(Name = " name " ) ] puЫic string Cl ient Name { get ; set; } [UIHint ( " Date " )] [Required(ErrorMe ss age = " Please enter а date " )] [Remote ("ValidateDate", "Home")] puЫic DateTime Date { get ; set; } [MustBeTrue(ErrorMes sage = " Уои mu s t accept t he terms " ) ] puЬlic bool TermsAccepted { get; set ; }
860 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC В качестве аргументов атрибуту Re mo t e указываются имя действия и контроллер, предназначенные для генерации URL, по которому библиотека проверки достов е рнос­ ти JavaScript будет обращаться с целью выполнения проверки - в рассматриваемом случае это действие Va l idate Date из контроллера Home . Чтобы посмотреть на работу удаленной проверки достоверности, запустите при­ ложение, п ерейдите на URL вида /Home и введите дату, относящуюся к прошлому. После того как фокус ввода переместится на другой элемент, появится сообщение об ошибке проверки достоверности (рис. 27.8). С1 Mod~1 Valld-'tion х 1 ~ .. с-~~~'"',:;;;~~~'-·· ~:::-=:.э ··1 1 Yoшname: 1 1 Joe 1 1 App oi ntment Date: 1 , 1 1 · Please enler а date iп the fulure 1 01/01/2000 1 , О 1accept the terms & coпditions 1 1 ,.,,,,,, . L--- ··- ---·-·· --- --·-- ---······--·-· ----··-·-· -- . ---··--. ·- -· -- _\ Рис. 27 .8 . Выполнение удаленной проверки достоверности Внимание! Метод действия для проверки достоверности будет вызываться, когда пользова­ тель впервые отправляет форму, и затем каждый раз , когда данные редактируются . Для элементов ввода текста любое нажатие клавиши будет иметь следствием обращение к серверу. В некоторых приложениях это может вылиться в значительное количество запро­ сов, что должно быть учтено при описании требований к производительности сервера и ширине полосы пропускания в производственной среде . Кроме того, может быть принято решение отказаться от удаленной проверки достоверности для свойств, проверка кото­ рых сопряжена с высокими затратами (например, если выяснение уникальности имени пользователя требует обращения к медленному серверу). Резюме В настоящей главе был представлен широкий спектр приемов , доступных для выполнения проверки достоверности моделей, которая гарантирует, что предостав ­ ляемые пользователем данные удовлетворяют ограничениям, налагаемым на модель данных. Пров ерка достоверности моделей является важной темой, и наличи е в при­ ложении подходящих средств проверки жизненно важно для обеспечения пользова­ телям комфортных условий работы. В следующей главе будет показано, как защитить приложение МVС с применением ASP.NET Соге Identity.
ГЛАВА 28 Введение в ASP.NET Core Identity с истема ASP.NET Core Identity представляет собой АРI-интерфейс от Microsoft, предназначенный для управления пользователями в приложениях ASP.NET. В этой главе демонстрируется процесс настройки ASP.NET Саге Identity и создания простого инструмента администрирования пользователей, который управляет инди­ видуальными пользовательскими учетными записями, хранящимися в базе данных . Система ASP.NET Core Identity поддерживает другие виды пользовательских учет­ ных записей, такие как записи, хранящиеся с использованием Active Dlrectory, но здесь они не рассматриваются, потому что редко применяются вне корпораций (где р еализации Active Directive оказываются настолько замысловатыми, что очень трудно отыскать полезные общие примеры). На заметку! Настоящая глава требует наличия установленного средства SQL Server LocalDB для Visual Studio. Чтобы добавить LocalDB, запустите программу установки Visual Studio и установите компонент Microsoft SQL Server Data Tools (Инструменты данных Microsoft SQL Server) . В главе 29 будет показано, как выполнять аутентификацию и авторизацию с по­ мощью таких пользовательских учетных записей, а в главе 30 - каким образом вый­ ти за рамки основ и применять ряд более сложных при емов. В табл. 28. 1 приведена сводка, позволяющая поместить систему ASP.NET Core Identity в контекст. Таблица 28.1 . Помещение системы ASP.NET Core ldentity в контекст Вопрос Что это такое? Чем она полезна? Как она используется? Ответ Система ASP.NET Core ldentity - это АРl-интерфейс для управления пользователями и запоминания пользовательских данных в хра­ нилищах, таких как реляционные базы данных, посредством Entity Framework Core Управление пользователями является важной возможностью для большинства приложений, и ASP.NET Core ldeпtity предлагает готовую хорошо протестированную платформу, которая не требует создания специальных версий распространенных функций Система ldentity используется через службы и промежуточное ПО , до­ бавляемое в класс Star tup, и посредством классов, которые действу­ ют в качестве шлюзов между приложением и функциональностью ldentity
862 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Окончание табл. 28 .1 Вопрос Ответ Существуют ли какие-то скры­ тые ловушки или ограничения? В Microsoft скомпенсировали жесткость ранних АРl-интерфейсов уп­ равления пользователями ASP.NET, сделав систему ldeпtity настолько гибкой и конфигурируемой, что выяснение того, что возможно и что необходимо, может оказаться проблематичным . В книге мы лишь слегка коснемся поверхности этой глубокой и сложной систе м ы Существуют ли альтернативы? Можно было бы реализовать собственные АРl-интерфейсы, но такая задача обычно требует большого объема работы , к тому же чревата со­ зданием уязвимостей защиты, если не выполнять ее крайне аккуратно Изменилась ли она по сравнению с версией MVC 5? Система ASP.NET Core ldeпtity работает с инфраструктурой ASP.N ET Core аналогично тому, как было в предшествующих версиях, хотя она обновлена для согласования с системой служб и промежуто ч но­ го ПО , а также имеет большее число компонентов, доступных через внедрение зависимостей . Сло ж ную авторизацию можно выполнять с помощью проверок , основанных на политиках и ресурсах, как будет описано в главе 30 В табл. 28.2 приведена сводка для настоящей главы. Таблица 28.2 . Сводка по главе Задача Добавление ldeпtity в проект Чтение пользовательски х данны х Создание пользовательской учетной записи Изменение стандартной поли­ тики проверки паролей Реализация специальной про­ верки паролей Изменение политики проверки учетных записей Реализация специальной про­ верки учетных записей Удаление пользовательской учетной записи Редактирование пользователь­ ской учетной записи Решение Добавьте пакеты и промежуточное ПО для ASP.NET ldentity Core и Entity Framework Core, создайте класс пользователя и класс контекста базы данных , а также создайте миграцию базы данных Выполните запрос к базе данных ldeпtity с применением класса контекста Вызовите метод UserManager . CreateAsync () Установите параметры паролей в классе Startup Реализуйте интерфейс IPasswordValidator либо унаследуйте класс от класса PasswordValidator Установите параметры пользовательски х учетных записей в классе Startup Реализуйте интерфейс IUserValidato r либо унаследуйте класс от класса UserValidator Вызовите метод UserManager.DeleteAsync() Вызовите метод UserManager . UpdateAsync() Листинг 28.1-28.15 28.16, 28.17 28.18-28 .20 28.21 28.22 -28.24 28 .25 28 .26-28 .28 28.29 , 28.30 28.31-28.33
Глава 28. Введение в ASP.NET Core ldentity 863 Подготовка проекта для примера Создайте новы й проект типа Empty (Пусто й) по имени Users с использовани­ ем шабл о на ASP.NET Core Web Application (.NET Core) (В е б-приложени е ASP.NET Core (.NET Core)) . Добавьте требуемые п акеты NuGet в р аздел dependen cies файла proj ect . j son и настройте инструментарий Razor в разделе t ool s, как показано в листинге 28.1 . Разделы, которые не нужны для данной главы , понадобится удалить. Пакеты , требующиеся для Identity, будут добавляться отдельно, чтобы подчеркнуть разницу между пакетами, необходимыми для общей разработки приложений MVC, и пакетами, предназначенными для аутентификации и авторизации. Л и стинг 28.1 . Добавление пакетов в файле proj ect. j son " dependencies ": ( " Microsof t . NETCo r e.App ": "version": "1.0.О", " type ": " platform" }, }1 " Microsoft.AspNetCore . Diagnostics " : " 1.0 . 0 ", " Microsoft . AspNetCore . Server. I IS i ntegration ": " 1 . 0 . 0 ", "Microsoft .AspNetCore .Server. Kestrel": "1.О.О", "Microsoft.Extensi ons .Logging .Console" : "1.0 .0", "Мicrosoft.AspNetCore.Mvc 11 : 11 1.0 . 0 11 , "Microsoft.AspNetCore . StaticFiles": "1.0.0", 11 Мicrosoft.AspNetCore . Razor.Tools 11 : "version": "1. О. O-preview2-final 11 , "type": "build" " tools ": 11 Мicrosoft.AspNetCore.Razor.Tools 11 : 11 1.0.0 -preview2-final 11 , " Mi crosoft . AspNetCore . Server . I I Si ntegration . Tools ": " l . O. O -preview2 - fi nal " }' 11 f r ameworks 11 : { " netcoreappl . O": " imports 11 : [ " dotnetS. 6 ", 11 portaЫe-net45+win8 "] )' "buildOptions ": 11 emitEntryPoint 11 : true , 11 preserveCompilationContext ": true )' " runtimeOpti ons ": { " configProperties ": { 11 System . GC . Server ": true } В листинге 28.2 показан код класса Startup, который конфигурирует средства, пр едоставляемы е пак етами NuG et.
864 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 28.2 . Содержимое файла Startup. cs using Microsoft.AspNetCore.Builder; using Microsoft .Ext ensions.Dependencyinjection ; namespace Users { puЬlic class Startup puЫic void ConfigureServices{IServiceCol lection services) { services.AddМvc(); puЫic void Configure(IAppli cationBuilder арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseМvcWithDefaultRoute(); } Создание контроллера и представления Создайте папку Controllers, добавьте файл класса по имени HomeController. cs и поместите в него определение контроллера из листинга 28.З. Этот контроллер будет применяться для описания деталей пользовательских учетных записей и данных, а его метод действия Index () передает словарь значений стандартному представле­ нию через метод View (). Листинг 28.З. Содержимое файла HomeController. cs из папки Controllers using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; namespace Users .Controllers { puЫic class HomeController : Controller puЫic ViewResul t Index () => View(new Dictionary<string, object> { [ " Placeholder "] = "Placeholder" } ) ; Чтобы снабдить контроллер представлением, создайте папку Vi ews/Home и до­ бавьте в нее файл представления по имени Index. cshtml с разметкой, приведенной в листинге 28.4 . Листинг 28.4 . Содержимое файла Index. cshtml из папки Views/Home @model Dictionary<string, object> <div class= " bg-primary panel-body"><h4>User Details</h4></div> <tаЫе class="taЫe taЬle-condensed taЫe-bordered"> @foreach (var kvp in Model) { <tr><th>@kvp.Key</th><td>@kvp.Value</td></tr> </tаЫе>
Глава 28. Введение в ASP.NET Саге ldentity 865 Пр едставлен ие отображает содержимое словаря модели в таблице . Для подде ­ ржки пр едставл ения создайте папку Views/ Shared и добавьте в нее файл по имени _ Layout . csh t ml с раз меткой , показанной в листинге 28 .5. Листинг 28.5 . Содержимое файла_Layout. cshtml из папки Views/Shared < ! DOCTYPE html> <html> <head> <t i tle>Users</tit l e> <meta name= " viewport " conten t= " wi dth=device - width " /> <link href= " /liЬ/bootstrap/d i st / css / boo t st r ap . css " rel= " stylesheet " /> </head> <body class= " pane l-body " > @RenderBody () </body> </html> При стили за ции НТМL- э л е ментов представлени е полаг аетс я на СSS-пакет Bootstrap . Создайт е в 1юрн евой папке проекта файл bower . j son с использованием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па­ кет Bootstrap в ра здел depende n c i es (листинг 28.6). Листинг 28.б. Добавление пакета Bootstrap в файле bower. j son " пате ": " asp.net ", " private ": true , " dependencies " : { "bootstrap": 11 3.3 .6 11 Последний подготовительный шаг связан с созданием файл а _V i ewimports . cshtml в п апк е Views, в котором настраиваются встроенные д е снрипторны е вспо­ могательные массы для применения в представлениях (листинг 28.7). Листинг 28. 7. Содержимое файла _Viewimports. cshtml из папки Views @addTagHelper * , Microsoft . AspNetCore . Mvc.TagHe l pers Наконец, создайте в папке Views файл запуска представле ния по имени _ ViewStart. cshtm l с содержимым из листинга 28.8 . Он обесп ечит использовани е компоновки, созданной в листинге 28.5, всеми представл ениями в приложении. Листинг 28.8 . Содержимое файла_ViewStart. cshtml из папки Views @{ Layout " Layout "; Запустив приложение , вы увидите вывод , приведенный на рис. 28.1 .
866 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Use rs х r::-- . .:;.. - ~ С CJ localhost :53221 Placeholder Placeholder Рис. 28.1. Запуск примера приложения Настройка ASP.NET Core ldentity Процесс настройки системы Identity затрагивает почти каждую часть приложе­ ния, требуя новых классов модели, изменений конфигурации, а также контроллеров и действий для поддержки операций аутентификации и 'авторизации. В последующих разделах мы пройдем по процессу настройки Identity в базовой конфигурации, что­ бы продемонстрировать разнообразные шаги, которые с ним связаны. Задействовать систему Identity в приложении можно многими разными способами, но конфигура­ ция, применяемая в этой главе, предусматривает использование самых простых и хо­ довых параметров. Добавление пакета ldentity в приложение В случае применения шаблона Empty среда Visual Studio не добавляет пакеты ASP.NET Core Identity или Entity Framework Core в создаваемые проекты, поэтому они должны быть добавлены вручную. Добавьте требуемые пакеты в файл proj ect. j son (листинг 28.9). Листинг 28.9. Добавление пакетов ldentity в файле project. json "dependenci е s" : { "Mi crosoft.NETCo re.App ": "version ": "1. О. О ", "type ": "p latform " }' " Microsoft . AspNetCore .Diagnostics": "1. 0 .О", " Microsoft.AspNetCore .Se rver .IISintegration ": "1. 0 . 0 ", "Microsoft .Asp NetCore .Server.Kestrel": "1.0.О", " Microsoft.Extens i ons .Logging .Console": "1.0. О ", "Micros oft.AspNetCore .Mvc": "1. 0 .0", "Mi crosoft .As pNetCore . StaticFiles": " 1 . О . О ", " Microsoft .AspNe tCore.Razor .T ools ": }' "vers i on ": "l .0 . 0 -p review2 -final", " type ": "build "
}1 Глава 28. Введение в ASP.NET Саге ldentity 867 "Мicrosoft.Extensions.Configuration": "1.0.0", "Мic r osoft . Extensions.Configuration.Json": 11 1.0.0 11 , "Microsoft.AspNetCore.Identity.EntityFrameworkCore": 11 1.0.0 11 1 "Мicrosoft . EntityFrameworkCore.SqlServer": 11 1.0.0 11 1 "Мicrosoft.EntityFrameworkCore.Tools": "l.0 .0 -preview2-final" "tools": { " Microsoft . AspNetCore . Razor .Tools ": " 1 . 0 . 0 - preview2 - final ", "Microsoft .AspNetCore . Server .IISintegration .T oo ls": " l .0. 0 - preview2 - f i nal ", "Мicrosoft.EntityFrameworkCore.Tools": "l.0 .0 -preview2-final" }1 " frameworks ": { " netcoreappl.0 ": "imports": [ "dotnet5.6 11 1 "portaЬle-net45+win8" ] }1 " buildOptions ": " emitEntryPoint ": true , " preserveCompi l ationCon text ": t r u e }1 " ru n timeOptions ": { " configProperti es ": { "Syst em . GC .Server": t r ue} У1шзанны е пакеты добавляют в проект ASP.NET Core Id entity и Entity Framework Core. Доб а вл ени е в разделе too l s устанавливает инструм енты командной строки, котор ые по зволяют настр аивать базу данных , используемую для хран ения данных Id entity, что вскор е будет сделано. Создан ие класса пользо вателя Сл едующий ш аr заключа ется в опр еделении класса , предназн ач е нного для пр ед­ ставления пользователя в приложении , который называется классом пользовате­ ля. Класс п ол ьз ователя: наследу ет ся: от класса I dentityUser, опр едел е нного в про ­ странств е имен Micro s oft . AspNetCore . Identi ty . En tityFrameworkCore . Кл асс Identi tyUser обеспечивает базовое представление пользователя , которое можно расширять, добавляя свойства к производному классу, как будет описано в глав е 30. В табл. 28.3 пер е числ ены наиболее пол езные встро енные свойства, которы е опр еде­ лены в Identi tyUser , включая применяемые в этой главе. И ндивидуал ьные свойства в настоящий момент неважны. Важно то, что кл асс IdentityUser пр едоставляет доступ к б азовой информ а ции о пользов ател е: имя пол ьзователя, адрес электронной почты, телефонный номер, хеш пароля, членство в рол я х и т. д . При желании хранить дополнительные св едения о поль з овател е придется добавить свойства в кла сс, унасл едованный от Identi tyUser, который будет исполь­ зоваться для представл ения пользователей в приложении. Чтобы со здать класс пользователя для приложения, со здайте папку Mode l s и до­ бавьте в нее файл класса по имени AppUser . cs с определени ем кл асса AppUser , при­ веденным в листинге 28 . 10 .
868 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 28.З. Свойства класса IdentityUser Имя Id UserName Claims Email Logins PasswordHash Roles PhoneNumЬer SecurityStamp Описание Это свойство содержит уникальный идентификатор пользователя Это свойство возвращает имя пользователя Это свойство возвращает коллекцию заявок (claim) пользователя, которая будет описана в главе 30 Это свойство содержит адрес электронной почты пользователя Это свойство возвращает коллекцию входов пользователя, используе­ мую для сторонней аутентификации, как объясняется в главе 30 Это свойство возвращает хешированную форму пароля пользователя , которая применяется в разделе "Реализация возможности редактиро­ вания" далее в главе Это свойство возвращает коллекцию ролей , к которым принадлежит пользователь (глава 29) Это свойство возвращает телефонный номер пользователя Это свойство возвращает значение, которое изменяется , когда меняет­ ся удостоверение пользователя, например, из-за смены пароля Листинг 28.1 О. Содержимое файла AppUser. cs из папки Model s using Microsoft.AspNetCore.Identity .E ntityFrameworkCore ; namespace Users . Models { puЬlic class AppUser : IdentityUser 11 Для базовой установки Identity 11 дополнительные члены не требуются На данный м омент имеется все, что нужно, хотя мы еще вернемся к классу AppUser в главе 30 при рассмотрении способа добавления свойств пользовательских данных, специфичных для приложения. Конфигурирование импортирования представлений Несмотря на то что это не относится непосредственн о к настройке ASP.NET Core Identity, в следующем разделе мы будем работать с объектами AppUser в представ­ лениях. Чтобы упростить написание представл ений, добавьте про странств о имен Users . Models в файл импортирования представл ений (листинг 28.11). Листинг 28.11. Добавление пространства имен в файле_Viewimports. csh tml @using Users.Models @addTagHelper * , Microsoft . AspNetCore . Mvc . TagHelpers
Глава 28. Введение в ASP.NET Core ldentity 869 Создание класса контекста базы данных Следующий шаг предусматривает создание класса контекста базы данных Entity Framework Core, который оперирует с классом App User . Класс контекста унаследован от Ident i tyDbContext<T>, где т - IOiacc пользователя (Ap p User в текущем приме­ ре). Добавьте в папку Models файл класса по имени Appidenti tyDbCon t ex t . cs и определите в нем класс, как показано в листинге 28.12 . Листинг 28.12 . Содержимое файла Appidenti tyDЬContext. cs из папки Models using Mi crosoft . AspNetCore . Identity .EntityFrameworkCore ; using Microsoft . EntityFrameworkCore ; names p ace Users . Model s { puЬlic class AppidentityDbC ontex t : Ident i tyDbConte x t <App User> { puЫic AppidentityDbContext(DbContextOptions<AppidentityDbContext> options) : base (options) { } Класс контекста базы данных может быть расширен для изменения способа, ко­ торым база данных настраивается и используется, но в случае элементарного прило­ жения ASP.NET Core Identity простого определения класса вполне достаточно, чтобы начать и получить заполнитель для любой настройки в будущем. На заметку! Не пере живайте, если роль та ких классов не особенно ясна. Если ранее вы не имели дела с инфраструктурой Entity Framework Core, тогда можете трактовать ее как "черный ящик ". После того, как основные строительные блоки окажутся на месте (и вы можете их копировать в свои проекты, чтобы все работало), потребность в их редактиро­ вании будет возникать редко . Конфигурирование настройки строки подключения к базе данных Первый шаг по конфигурированию ASP.NET Core Identity заключается в опреде­ лении строки под1<mочения к базе данных. По соглашению строка подключения по­ мещается в файл appsettings . j son, который затем загружается в I\Лассе Startup. Создайте в корневой папке проекта файл appsettings. j son с использованием шаб­ лона элем ента ASP.NET Configuration File (Файл конфигурации ASP.NET) и добавьте в него содержимое листинга 28.13. Листинг 28.1 З. Содержимое файла appsettings. j son "Data": { " SportStoreidentity ": " ConnectionString ": " Server=(localdb)\\MSSQLLocalDB;Database= IdentityUsers ; Trusted_Connec tion=Tr ue; Mu l tipleAc tiveResultSets =true " }
870 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC В строке подключения указан параметр loca l d b, который предоставляет удобную поддержку баз данных для разработчиков. Кроме того, в качестве имени базы данных ука з ывается IdentityUsers. На заметку! Ширина печатной страницы не позволяет соблюдать правильный формат строки подключения, которая дол ж на выглядеть как одиночная неразрывная строка. В редакто­ ре Visual Studio это не проблема, но в листинге строку пришлось разбить на части. При добавлении строки подключения в свой проект удостоверьтесь, что вводите ее в единс­ твенной строке. Имея строку подключения к базе данных, можно обновить класс Startup для чте ­ ния конфигурационного файла и обеспечения доступности настроек (листинг 28.14). Листинг 28.14 . Чтение настроек приложения в файле Startup. cs u sing Mi crosoft . AspNe tCore . Bu ilde r; using Microsoft .E xtens i ons . Depend e n cy! n j ection ; using Мicrosoft.Extensions . Configuration; using Мicrosoft.AspNetCore.Hosting; namespace Users { puЫ i c clas s Startup IConfigurationRoot Configuration; puЫic Startup(IHostingEnvironment env) Configuration = new ConfigurationBuilder() .SetвasePath(env.ContentRootPath) .A ddJsonFile ("appsettings. json") .Build(); puЫi c void Con figu reSe rvices (IServ iceCollection s e r v i ces ) { services . AddMvc(); puЫ ic void Con figu re(IApplication Bui lder арр ) { a pp.UseStat usCodePages (); ap p. UseDeveloperExcept ionPage() ; app.UseStaticFiles(); a pp.U seMv cWithDef a ul tRou te (); Внесенные в код изменения загружают файл a pp s ett i ngs . j son и предст авляют сод е ржимое через свойство Conf ig ura t i on , которое применяется в следующем ра з ­ дел е при настройке служб и промежуточного ПО ASP.NET Core Identity. Конфигурирование служб и промежуточного программного обеспечения ldentity Финальный шаг настройки системы Identity пр едусматривает добавление служб и промежуточного ПО в нласс S t a rtup , чтобы внедрить Identity в конве йер обработни запросов и предоставить средства, которые используются для управл е ния пользов а ­ телями где-то в других местах приложения. Необходимые изменения поназаны в лис­ тинге 28.15.
Глава 28 . Введение в ASP.NET Core ldentity 871 Листинг 28.15. Конфигурирование служб и промежуточного ПО ASP.NET Core ldentity в файле Startup. cs using Microsoft . AspNetCore.Builder ; using Microsoft . Extensions . Dependencyinjection ; using Microsoft . Extensions . Configurat i on ; using Microsoft . AspNetCore .H osting ; using Мicrosoft.EntityFrameworkCore; using Microsoft . AspNetCore.Identity.EntityFrameworkCore; using Users.Model s ; namespace Users { puЫic class Startup IConfigurationRoot Configuration; puЬlic Startup(IHostingEnvironment env) { Configuration = new Configu r ationBuilder() . SetBasePath(env .C ontentRootPath) . A ddJsonFile( " appsettings . json " ) . Build() ; puЫic void ConfigureServices(IServiceCollection services) services.AddDЬContext<AppidentityDЬContext>(options => options.UseSqlServer( Configuration["Data:SportStoreidentity:ConnectionString"])); services.Addidentity<AppUser, IdentityRole>() . A d dEntityFrameworkStores<AppidentityDbContext>(); services.AddMvc() ; puЫic void Configur e(IApplicat ionBuilder арр) ( app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app.UseStaticFi l es() ; app .U seidentity() ; app.UseMvcWithDefaultRoute() ; Для создания базовой установки ASP.NET Core Identity требуются три набора изме ­ нений. Сначала настраивается инфраструктура Entity Framework (EF) Core, которая предоставляет приложениям МVС службы доступа к данным: services . AddDbContext<App i dentity DbContext>(options => options . UseSqlServer(Configurat i on[ " Data:SportStoreidentity : ConnectionString " ])) ; Метод AddDbCon text () добавляет службы, требующиеся для EF, а метод UseSglServer () настраивает поддержку, необходимую для хранения данных с при­ менением Microsoft SQL Server. Метод AddDbContext () позволяет использовать ранее созданный класс контекста базы данных и указать, что он будет копироваться с ба­ зой данных SQL Server, строка подключения для которой получа ет ся из конфигурации при ложения (в пример е приложения это файл appsettings . j son) .
872 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Понадобится также настроить службы для ASP.NET Core Identity, что делается сле­ дующим образом: services.Addidentity<AppUs e r, IdentityRole>() .Ad dEntityFramewo rkSt ores<Appiden t ityDbCo nt ext >() ; Метод Addidenti ty () имеет параметры типов, которые указывают класс, приме­ няемый для представления пользователей, и класс, используемый для представления ролей. Здесь задается класс AppUser для пользователей и класс Ide ntityRo l e для ролей. Метод AddEnti tyFrameworkStores () указывает, что система Identity долж­ на использовать инфраструктуру Entity Framework Core для сохранения и извлече­ ния своих данных с применением созданного ранее класса контекста базы данных. Последнее изменение в ю~ассе Startup касается добавления системы ASP.NET Core Identity в конвейер обработ1<и запросов . Это позволяет ассоциировать пользователь­ ские учетные данные с запросами на основе сооkiе-наборов или переписывания URL, т.е. детали пользовательских учетных записей не включаются напрямую в НТТР­ запросы, отправляемые приложению, или в ответы, которые оно генерирует: app . Useidentity() ; Создание базы данных ldentity Почти все на месте, и осталось лишь фактически создать базу данных, которая будет использоваться для хранения данных Identity. Откройте окно консоли диспет­ чера пакетов, выбрав пункт меню Toolsc::>NuGet Package Manager (Сервис е::> Диспетчер пакетов NuGet) в Visual Studio, и введите следующую команду: Add - Migration Initial Как объяснялось при настройке базы данных для приложения SportsStore. инфра­ структура Eпtity Framework Core управляет изменениями в схемах баз данных через средство, которое называется миграции. В случае модификации 1тассов модели, при­ меняемых для генерации схемы, можно сгенерировать файл миграции, 1<0торый содер­ жит команды SQL, предназначенные для обновления базы данных. Приведенная выше команда создает файлы миграции, которые будут настраивать базу данных Identity. Когда команда завершит выполнение. вы увидите в окне Solution Explorer папку Migrations. Просмотрев содержимое файлов в этой папке, вы обнаружите команды SQL, которые будут использоваться для создания начальной базы данных. Чтобы за­ действовать файлы миграции для создания базы данных, введите такую команду: Update - Database Выполнение команды может занять некоторое время, а после ее завершения база данных будет создана и готова к применению. Использование ASP.NET Core ldentity После проведения базовой настройки можно приступать к применению ASP. NEТ Core Identity для добавления поддерж1ш управления пользователями в пример при­ ложения. В последующих разделах будет продемонстрировано, как использовать АР!­ интерфейс Identity для создания инструментов администрирования, которые делают возможным централизованно е управление пользователями.
Глава 28. Введение в ASP.NET Соге ldentity 873 Инструменты централизованного администрирования пользователей полезны практически во всех приложениях , даже в тех, которые позволяют пользователям со­ здавать и управлять собственными учетными записями. Всегда найдутся пользовате­ ли. которым требуется, к примеру , пакетное создание учетных записей и поддержка задач, предполагающих инспектирование и корректировку пользовательских данных. В рамках настоящей главы инструменты администрирования удобны из-за того, что они организуют множество основных функций управления пользователями в неболь­ шое число классов, делая их полезными примерами для демонстрации фундаменталь­ ных возможностей системы ASP.NET Core Identity. Перечисление пользовательских учетных записей Отправной точкой этого раздела является перечисление всех пользовательских учетных з аписей в базе данных, что позволит увидеть эффект от кода, который будет добавлен в приложение позже. Первым делом добавьте в папку Co nt ro llers файл класса по имени AdminC o ntr ol l er . c s и определите в нем контроллер, как пшшзано в листинге 28 . 16, который будет применяться для реализации функциональности ад­ министрирования пользователей. Листинг 28.16. Содержимое файла AdminController. cs из папки Controllers using Mi c r osoft. AspNetCore . I de ntity ; using Microsoft. AspN etCore . Mvc ; using Users .Models; namespace Use r s .Contro llers { puЫic cl ass AdminControll er : Controller { private UserManager<AppUser > userManager; puЬlic Admi nCont roller(UserManager<AppUser> us rMgr) { userManage r = usrMg r; puЫic ViewRes u lt I ndex() => Vi e w(userMa n ager. Users) ; Метод действия Ind ex () перечисляет пользователей, управляемых систе­ мой I dentity; разумеется, в текущий момент пользователи отсутствуют, но вско­ ре они появятся . Доступ к поль з овательским данным осуществляется через объе1<Т UserManager<Ap p Us er> . который конструктор контроллера получает поср едством внедрения зависимостей . С помощью объекта UserManage r <App Us er> можно запрашивать хранилище данных. Свойство User s возвращает перечисление объектов пользователей (экземп­ ляров класса AppUser в рассматриваемом приложении), с которым удобно работать, используя LINQ. Внутри метода действия значение свойства Us er s, которое будет пе­ речислять всех пользователей в базе данных, передается методу View (),так что он сможет отобразить детали учетных записей. Чтобы снабдить метод действия пред­ ставлением, создайте папку Vi ew s /Admi n , добавьте файл по имени Inde x . c sht ml и поместите в него разметку из листинга 28.17.
874 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 26.17. Содержимое файла Index.cshtrnl из папки Views/Adrnin @model IEnumeraЫe<AppUser> <div class= "b g-primary panel-body"><h4>User Accounts</h4></div> <tаЫе class= " taЫe taЫe-condensed taЬle - bordered " > <tr><th>ID</th><th>Name</th><th>Email</th></tr> @if (Model.Count() == 0) { <tr><td colspan= " З " class= " text-center " >No User Accounts</td></tr> else { foreach (AppUser user in Model) { < tr> <td>@user.Id</td><td>@user.UserName</td><td>@user.Email</td> </t r> ) </tаЫе> <а class= " btn Ьtn - primary " asp-action= " Create " >Create</a> Представление содержит таблицу, в которой для каждого пользователя предусмот ­ рена строка с колонками, отображающими уникальный идентификатор, имя пользо­ вателя и адрес электронной почты. Если пользователи в базе данных отсутствуют. тогда выводится соответствующее сообщение, как показано на рис. 28.2, для чего по­ надобится запустить прилож ение и запросить URL вида / Admin . +- -i" С 1 [j localhost:53221/Adinin ID Name Email No User Accounts '•' Рис. 28.2 . Отображение (пустого) списка пользователей В представлени е включена ссылка Create (Со здат ь), стилизованная под кнопку, которая нацелена на действие Create контроллера Admin. Это действие будет подд е ­ ржив ать добавление пользователей.
Глава 28. Введение в ASP.NET Core ldentity 875 Переустановка базы данных Запустив приложение и перейдя на URL вида / Admin, вы заметите небольшую паузу перед отображением содержимого, визуализируемого из представления . Причина в том, что инф­ раструктура Entity Framework Core должна создать и подготовить базу данных к ее первому при менени ю. Увидеть созданную базу данны х можно, открыв окно SQL Server Object Explorer (Проводник объектов SQL Server) в Visual Studio . Если вы впервые используете окно SQL Server Object Explorer, то должны выбрать в меню Tools (Сервис) пункт Connect to Database (Подключиться к базе данных) , чтобы сообщить среде Visual Studio о базе данных, с кото­ рой нужно работать. В качестве источника данных выберите Microsoft SQL Server, для имени сервера ука жите ( localdЬ) \mssqllocaldЬ , оставьте отмеченным флажок Use Windows Authentication (Использовать аутентификацию Windows) и щелкните на стрелке, раскрыва­ ющей поле Select Or Enter а Database Name (Выберите или введите имя базы данных) . Спустя несколько секунд отобразится список доступных баз данных LocalDB, в котором должна быть возможность выбора базы данных IdentityUsers, относящейся к примеру приложения . Щелкните на кнопке ОК , после чего в окне SQL Server Object Explorer поя­ вится новая запись. Среда Visual Studio запомнит базу данных, так что описанный процесс необходимо выполнить толь ко один раз . Для просмотра базы данных понадобится раскрыть элемент ( localdЬ) \ms sqllocaldb<> Databases <>Identi tyUsers в окне SQL Server Object Explorer. Вы получите воз­ можность увидеть таблицы , которые были созданы файлами миграции, с именами вроде AspNetusers и AspNetRoles . После добавления пользователей, как будет показано в следующем разделе, в базу данных можно отправлять запросы для просмотра содержимого таблиц. Чтобы удалить базу данных, щелкните правой кнопкой мыши на элементе Identi tyusers в окне SQL Server Object Explorer и выберите в контекстном меню пункт Delete (Удалить) . В диалоговом окне Delete Database (Удаление базы данных) отметьте оба флажка и щелк­ ните на кнопке ОК для удаления базы данных. Чтобы заново создать базу данных , откройте окно консоли диспетчера пакетов и введите следующую команду: Update - Database База данны х будет воссоздана и готова к применению, когда вы снова запустите приложение . Создание пользователей В отношении входных данных, получаемых приложением, будет использоваться проверка достоверности моделей MVC, и легче всего это сделать за счет создания про­ стых моделей представлений для каждой операции, поддерживаемой контроллером . Создайте в папке Models файл IOJacca по имени UserViewModels . cs с содержимым, приведенным в листинге 28.18. Листинг 28.18. Содержимое файла UserViewModels. cs из папки Models using System.ComponentModel . DataAnnotations; namespace users . Models { puЫic class CreateModel { [Required]
876 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic string Name { get ; set; ) [Required] puЫic string Email { get ; set ; } [Required] puЫic string Password { get ; set ; Начальная модель называется Crea teModel и определяет основные свойства, которые требуются для создания пользовательской учетной записи: имя пользова­ теля, адрес электронной почты и пароль. Атрибут Required из пространства имен Systern. CornponentModel . DataAnnotations применяется для указания на обяза­ тельность значений всех трех свойств, определя емых моделью. В листинге 28.19 к контроллеру Adrnin добавле на пара методов действий Create (} , на которые нацелена ссылка в представлении Index из предыдущего ра здел а ; они ис­ пользуют стандартный прием для отображения пользовател ю представления в случае запроса GET и обработки данных формы при поступлении запроса POST. Листинг 28.19. Определение методов действий Create () в файле AdminController. cs using Microsoft . AspNetCore . Identity ; using Microsoft . AspNetCore . Mvc ; using Use rs . Models; using System.Threading.Tasks ; namespace Users . Controllers { puЫic class AdminController : Controller { private UserManager<AppUser> userManager; puЫic AdminController(UserManager<AppUser> usrMgr) { userManager = usrMgr ; puЫic ViewResu l t Index () => View(use rManager.Users ) ; puЬlic ViewResult Create () => View () ; [HttpPost] puЫic async Task<IActionResult> Create(CreateModel model) if (ModelState.IsValid) { AppUser user = new AppUser UserName = model.Name, Email = model . Email }; IdentityResult result = await userМanager.CreateAsync(user, model.Password); if (result.Succeeded) { return RedirectToAction("Index"); else { foreach (IdentityError error in result.Errors) ModelState.AddМodelError("", error.Description); } return View(model);
Глава 28 . Введение в ASP. NET Core ldeпtity 877 Важной частью листинга является метод действия Crea te () , принимающий аргумент CreateModel, который будет вызываться, когда администратор отправ­ ляет данные формы. Свойство ModelState. IsValid применяется для проверки того, что данные содержат обязательные значения, и если это так, тогда создает­ ся новый экземпляр класса AppUser, который передается асинхронному методу UserManager.CreateAsync() : AppUser user = new AppUser { UserNarne = rnodel . Narne , Ernail = rnodel . Ernail }; IdentityResult result = await userМanager.CreateAsync(user, model.Password); В качестве результата метод CreateAsync () возвращает объект IdentityResult, который описывает исход операции посредством свойств, перечисленных в табл. 28.4 . Таблица 28.4 . Свойства класса IdentityResul t Имя Описание Succeeded Возвращает t rue, если операция выполнилась успешно Errors Возвращает последовательность объектов I denti tyError, описываю­ щих ошибки, которые возникли при попытке выполнения операции. Класс Ident i tyEr r or предлагает свойство Description со сводкой по проблеме В методе действия Create () с помощью свойства Succeeded выясняется , была ли создана новая запись о пользователе в базе данных. Если свойство Succeeded воз­ вращает true, тогда клиент перенаправляется на действие Index , так что отобразит­ ся список пользователей: if (result.Succeeded) return RedirectToAction("Index"); else { foreach (Iden t ityError error in result . Errors) ModelState . AddModelError( "", error .Description) ; Если свойство Succeeded возвращает false, тогда производится перечисление последовательности объектов I den t i t y Er ror , которую предоставляет свойство Errors . Свойство Description используется для создания ошибки проверки досто­ верности на уровне модели с помощью метода ModelState . AddMode l Error (), как объяснялось в главе 27. Чтобы снабдить новые методы действий представлением, создайте в папке Views/Adrnin файл представления по имени Create . cshtrnl и добавьте в него раз­ метку, п01шзанную в листинге 28.20. Листинг 28.20. Содержимое файла Create. cshtml из папки Views/Admin @model CreateModel <div class= " bg - prirnary panel - body " ><h4>Create User</h4></div> <div asp - validation -s ummary= " All " class= " text - dange r" ></div>
878 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC <fo rm asp - action= " Create" method="post " > <di v class= " form- group" > <label asp -f or= " Na me" ></la bel > <input asp -for= " Name " class= " fo rm- control " /> </di v> <d iv class= "form- group " > <label asp -for= " Email " ></label> <input asp -for= " Email " class="form-control" /> </div> <div cl ass=" form- group"> <label asp -f or= "P assword " ></label> <input asp -for= "Pa sswo rd" class ="form - control " /> </div> <button type="submit" class= " Ьtn Ьtn -prim ary " >Create</button> <а asp - action="Index " class="btn btn-defau lt">Cancel</a > </ form > В этом представлении нет ничего особо примечательного - в нем присутствует простая форма для сбора значений, которые инфраструктура MVC привяжет к свойс­ твам объекта модели, переданного методу действия Crea te ( ) , а также сводка по воз­ можным ошибкам проверки достоверности. Тестирование функциональности создания Чтобы протестировать возможность создания новой пользовательской учетной за­ писи, запустите приложение, перейдите на URL вида / Admi n и щелкните на кнопке Create (Создать). Заполните форму значениями из табл. 28.5 . Таблица 28.5 . Значения для создания примера пользователя Имя Name Email Password Описание Joe joe@example . com Secretl23$ Совет. Существуют домены, зарезервированные для целей тестирования, в том числе example . сот . Полный список таки х доменов доступен по адресу https : / /tools . ie tf. o rg/html/rfc2606 . После ввода значений щелкните на кнопке Create. Система ASP.NET Core ld entity создаст пользовательскую учетную запись, которая будет отображаться после перена­ правления браузера на метод действия Index () , что видно на рис. 28.3 . (Вы получите другой идентификатор, т.к. идентификаторы для пользовательских учетных записей генерируются случайным образом.) Щелкните на кнопке Create еще раз и введите в элементах формы значения из табл. 28.5 . На этот раз после отправки формы вы получите ошибку, о которой сооб­ щается в сводке по проверке достоверности модели (рис. 28.4).
Глава 28. Введение в ASP.NET Core ldentity 879 +- ~ С [J localh o st :532.21/Admin ~:= ю Na me Email 75c90f9b-aff7-4818-ab52-Зa640b4501 fd Joe joe@example .com 1ЫМ1 Рис. 28.З. Добавление новой пользовательской учетной записи +- ~ С ' CJ localhost:53221/Adniin/Create • User name 'Joe' is already taken . Name Joe Рис. 28.4 . Ошибка при попытке создать нового пользователя Проверка паролей Одним из наиболее распространенных требований, особенно в корпоративных приложениях, является принудительное применение политики проверки парол ей. Чтобы взглянуть на стандартную политику, запустите приложение, запросите URL вида /Admi n/Create и заполните форму данными, приведенными в табл. 28.6; важ­ ное отл ичие их от данных из предыдущего раздела связано со значением. вводимым в поле пароля.
880 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Таблица 28.6 . Значения для создания примера пользователя Имя Name Ernai l Описание Password Alice alice@example.com secret Когда форма отправляется серверу. система ldentity проверяет пароль-кандидат и генерирует ошибки, если он не удовлетворяет требованиям (рис . 28.5). Us~_rs х +- 7 С :a1oca l host : sз2 211Atimi11/Cre;;;----------~: : __________--,.:.;.:..;_."-..-·---------· .:.:.:........---..:.:. ,~.:.....;.:.:.:_:__:,._ ···-·- ---__ _ ____ ...__.:._ __ • Passwords must have at least опе поп letter and non digit charact er. • Passwords musl have at least one digit ('О' -'9'} . • Passwords must have at Jeas! one uppercase ('A'-'Z '). Name Alice Рис. 28.5 . Ошибки в результате проверки пароля Правила проверки парол ей можно сконфигурировать в классе Startup, как пока ­ зано в листинге 28.21 . Листинг 28.21. Конфигурирование правил проверки паролей в файле Startup. cs puЫic void ConfigureServices(IServiceCollection services) services . AddDbContext<AppidentityDbContext>(options = > options . UseSqlServer( Configuration ["Data : SportStoreidentity : ConnectionString " ])) ; services.Addidentity<AppUser, IdentityRole>(opts => opts.Password.RequiredLength = 6; opts.Password.RequireNonAlphanumeric = false; opts.Password.RequireLowercase = false; opts.Password.RequireUppercase = false; opts.Password.RequireDigit = false; }) .Add .EntityFrameworkStores<AppidentityDbContext>(); services . AddMvc() ;
Глава 28. Введение в ASP.NEТ Саге ldentity 881 Метод AddI den t i t у ( ) может использоваться с фующией, которая принима­ ет объект Identi tyOptions, чье свойство Password возвращает экземпляр класса PasswordOptions . Класс PasswordOptions предоставляет свойства для управления политикой пров ер ки паролей, описанные в табл. 28.7 . Таблица 28. 7 . Свойства класса PasswordOptions Имя RequiredLength RequireNonAlphanumeric RequireLowercase RequireUppercase RequireDigit Описание Это свойство типа int применяется для указания мини­ мальной длины паролей Установ ка этого свойства типа bool в true требует, чтобы пароли содержали хотя бы один символ, не являющийся буквой или цифрой Установка этого свойства типа bool в true требует, чтобы пароли содержали хотя бы один символ нижнего регистра Установка этого свойства типа bool в true требует, чтобы пароли содержали хотя бы один символ верхнего регистра Установка этого свойства типа bool в true требует, чтобы пароли содержали хотя бы один цифровой символ В листинге 28.21 указано, что пароли должны иметь минимальную длину в шесть символов , а другие ограничения отключены. Это не должно делаться в ре­ альном проекте, но позволяет получить эффективную демонстрацию. Запустив приложение, перейдя на URL вида /Admin/Create и повторив отправку формы , вы обнаружите, что пароль se c ret теперь принимается и новая пользовательская учет­ ная запись успешно создается (рис . 28.6). [') Us.ers х f- С 1ф localhost:623 i 1/Adn1in/Create ' (:( г---·--·-------- --·----·-·------ ~ --· --------- ·-~-=:=·=::о.=.:---==-==-::::==-..::;:::-=:-.:::=:.:.~-=:-·.=::.===-=--- -- • Password canr10t contaiп userriame • Password сап11оt contain numeric sequence Name ВоЬ Ри с. 28.6. Изменение политики проверки паролей
882 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Реализация специального класса проверки паролей Встроенной проверки паролей вполне достаточно для большинства приложений, но может возникнуть необходимость в реализации специальной политики, особенно при разработке корпоративного производственного приложения, в котором сложные политики проверки паролей - обычное явление. Функциональность проверки па­ ролей определяется интерфейсом IPasswordValidator<T> из пространства имен Microsoft . AspNetCore . Identi ty, где Т - класс пользователя, специфичный для приложения (AppUser в рассматриваемом примере): using System.Threading.Tasks ; namespace Microsoft . AspNetCore.Identity { puЫic interface IP asswordVa lidator<TUser> where TUser : class { Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user , string password); Для проверки пароля вызывается метод ValidateAsync (), которому передают­ ся данные контекста через объект UserManager (позволяющий выполнять запросы к базе данных Identity), представляющий объект пользователя и пароль-кандидат. В результате возвращается объект IdentityResult, создаваемый с использованием статического свойства Success, если проблемы отсутствуют, или вызывается стати­ ческий метод Failed (),которому передается массив объектов Identi tyError, опи­ сывающих возникшие во время проверки проблемы. Чтобы продемонстрировать применение специальной политики провер­ ки, создайте папку Infrastructure и добавьте в нее файл класса по имени CustomPasswordValidator . cs с определением из листинга 28.22 . Листинг 28.22 . Содержимое файла CustomPasswordValidator. cs из папки Infrastructure using System.Collections . Generic; using System.Threading.Tasks; us ing Microsoft.AspNetCore . Identity; using Users.Models; namespace Users .Infrastructure puЫic class CustomPasswordValidator : IPasswordValidator<AppUser> { puЫic Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user , string password) { List <IdentityError> errors = new List<IdentityError>(); if (password . ToLower() .Contains (user . UserName . ToLower() )) errors.Add(new IdentityError { Code = "Pa sswordContainsUserName ", Description = "Password cannot contain username" }); if (password.Contains("l2345")) errors . Add(new IdentityError { Code = "PasswordConta insSequence", Description = "Pas sword cannot contain numeric sequence " });
Глава 28 . Введение в ASP.NET Core ldentity 883 return Task . FromResult(errors . Count ==О? IdentityResult . Success : IdentityResul t .Failed(errors.ToArray()) ); К.ласе CustomPasswordValidator проверяет, не содержит ли пароль имя поль­ зователя или последовательность 12345 . В листинге 28.23 класс CustomPassword Validator р е гистрируется как средство проверки паролей для объектов AppUser. Листинг 28.23. Регистрация специального класса проверки паролей в файле Startup. cs using Microsoft .AspNetCore .B uilder ; using Microsoft . Extensions . Dependencyinjection; using Microsoft . Extensions . Configuration; using Microsoft . AspNetCore .H osting ; using Microsoft . EntityFrame workCore ; using Microsoft . AspNetCore .Identi t y.E nt ityF rameworkCore ; using Users . Models ; using Users.Infrastructure; using Мicrosoft.AspNetCore.Identity; namespace Users { puЬlic class Startup { IConfigurationRoot Con f iguration ; puЫic Startup(IHostingEnvironment env) { Configuration = new Configuration Bui l der() . SetBasePath(env . ContentRootPath) . A ddJsonFile( " appsettings . json " ) . Build() ; puЫic void ConfigureServices(IServiceCollection services) services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>(); services . AddDbContext<Appident i tyDbContext>(options => options . UseSqlServer( Configuration [" Data :Sp ortSto r e id entity:ConnectionString " ]) ) ; services . Addidentity<AppUser , IdentityRole>(opts => { opts . Password . RequiredLength = 6 ; opts . Password . RequireNonAl phanumeric = false; opts.Password . RequireLowerca se = false ; opts . Password . RequireUppercase = false; opts . Password . RequireDig it = false; }) . AddEntityFrameworkStores<AppidentityDbContext>() ; services.AddMvc(); puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . OseStaticFi les() ; app . Useidentity() ; app . UseMvcWithDefaultRoute() ;
884 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Чтобы протестировать специальную политику, запустите приложение, запросите URL вида /Admin/Create и заполните форму значениями, приведенными в табл. 28.8 . Таблица 28.8. Значения для создания примера пользователя Имя Narne Email Password Описание ВоЬ bob@exarnp le. com ЬоЫ2345 Пароль в табл. 28.8 нарушает оба правила проверки, навязываемые специальным классом, и вызывает отображение сообщений об ошибках , как показано на рис. 28.7 . С) Users х 1~ 1-- 1 i ! ! С 1 Ф localhost:623 11 /Adn; i11/C r~ate Password caлnot contain username • Password cannot contair1 rшmeric sequence Name ВоЬ Рис. 28. 7. Использование специального класса проверки паролей Можно также реализовать специальную политику проверки, построенную на ос­ нове встроенного класса , который применяется по умолчанию. Стандартный класс называется PasswordValidator и определен в пространстве имен Microsoft . AspNetCore . Identi ty . В листинге 28.24 специальный класс проверки изменен так, что теперь он унаследован от класса PasswordValidator и основывается на подде­ рживаемых им базовых проверках. Листинг 28.24. Наследование от встроенного класса проверки в файле CustornPasswordValidator. cs using System .C ollections .Ge neric ; us ing System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Users . Models ; using System.Linq; namespace Users.Infrastructure puЬlic class CustomPasswordValidator PasswordValidator<AppUser> {
Глава 28. Введение в ASP.NET Core ldentity 885 puЬlic override async Task<IdentityResult> ValidateAsync( UserManager<AppUser> manager, AppUser user , string password) { IdentityResult result = awaitbase .ValidateAsync(manager, user, password}; List<IdentityError> errors = result.Succeeded? new List<IdentityError>(} : result.Errors.ToList(}; if (password.ToLower() .Contains(user.UserName. ToLower())) errors . Add(new IdentityError { Code = 11 PasswordContainsUserName 11 , Description = 11 Password cannot contain username 11 }); if (password . Contains ( 11 12345 11 )) errors . Add(new IdentityError { Code = " PasswordContainsSequence 11 , Descripti on = 11 Password cannot contain numeric sequence 11 }); return errors . Count ==О ? IdentityResult .Success : IdentityResult . Failed(errors .ToArray()) ; Для тестирования объединенной проверки запустите приложение и заполните данными из табл. 28.9 форму, возвращаемую для URL вида /Admin/Create . Таблица 28.9 . Значения для создания примера пользователя Имя Name Email Password Описание ВоЬ bob@example.com 12345 После отправки формы вы увидите сочетание сообщений об ошибках специаль­ ной и встроенной проверки (рис. 28.8). [J Usш х J~ С ГQ-~~lhos~:S1296/дdn11ri/Cr,;;;;----- -------~ г ---'==--::-:::-:--==-=-=--"=-.-=-===:--_;__-=-::---·--' 1 1 • Pass\vords must Ье at least 6 characters. • Pass\vord canno1 con tain nLJmeric sequence Name ВоЬ Рис . 28.8 . Объединение специальной и встроенной провер ки паролей
886 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Проверка деталей, связанных с пользователем При создании учетной записи проверка выполняется также в отношении имени пользователя и адреса электронной почты. Чтобы взглянуть на работу встроенной пров е рки, запустите приложение , запросите URL вида /Admin/Crea t e и заполните форму данными из табл. 28.10. Таблица 28.1 О. Значения для создания примера пользователя Имя Name Ema il Passwo rd Описание ВоЫ ali ce@example . com secret Отправив форму. вы получите сообщение об ошибке (рис. 28.9). С) Uиrs • User name 'ВоЬ!' is invalid, сап only contain letters or digits. Name ВоЫ Email . ~ · ~ ..,,,,. ...,, ~"~.~.,,.,.,,..~ -#~- - ~. Рис. 28.9. Ошибка при проверке пользовательской учетной записи Проверку можно конфигурировать в классе Star tu p с использованием свойс­ тва I de nti tyOpt i o ns. Use r, которое возвращает экземпляр класса UserOptions. Свойства User Opt i ons описаны в табл. 28.11 . Таблица 28.11. Свойства класса UserOptions Имя AllowedU s e r Na meC haracter s RequireUn iqu eEma il Описание Это свойство типа s t r i n g содержит все разрешенные символы, которые могут применяться в имени пользова­ теля . В стандартном значении указаны символы a - z , A- Z , 0- 9 , переноса, точки, подчеркивания и @. Данное свойство не является регулярным выражением, и каждый разрешен­ ный символ должен задаваться явно в строке Установка этого свойства типа b ool в true требует, чтобы для новых учетных записей указывались адреса электрон­ ной почты, которые не использовались ранее
Глава 28. Введение в ASP.NET Core ldentity 887 В листинге 28.25 конфигурация приложения изменена так, чтобы уникальные адреса электронной почты были обязательными и в именах пользователей допус­ кались только алфавитные символы нижнего регистра. Листинг 28.25. Изменение настроек проверки пользовательских учетных записей в файле Startup. cs puЫic void ConfigureServices(IServiceCollection services) services.AddTransient<IPasswordValidator<AppUser> , CustomPasswordValidator>(); services.AddDbContext<AppidentityDbContext>(options => options.UseSqlServer( Configuration["Data : SportStoreidentity : ConnectionString"])); services.Addidentity<AppUser, IdentityRole>(opts => { opts.User.RequireUniqueEmai1 = true; opts.User.AllowedUserNameCharacters = 11 aЬcdefghijklmnopqrstuvwxyz 11 ; opts . Password.RequiredLength = 6; opts.Password . RequireNonAlphanumeric = false; opts.Password.RequireLowercase = false; opts.Password . RequireUppercase = false; opts.Password.RequireDigit = false; }) . AddEntityFrameworkStores<AppidentityDbContext>() ; services.AddMvc(); Повторно отправив данные из предыдущего теста, вы заметите, что адрес элект­ ронной почты сейчас приводит к ошибке, а символы в имени пользователя по-пре­ жнему отклоняются (рис. 28.10). D Users х f-с . €] ·--- -------·-------------·-------------------- • User пате 'ВоЬ!' is iпvalid. сап опlу сопtаiп letters or digits. • Email 'alice@example.com' is already takeп . Na me ВоЬ! Рис. 28.10. Изменение настроек проверки пользовательс ких учетных записей
888 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Реализация специальной проверки пользователей Функциональность проверки указывается с помощью интерфейса IUserValidator<T>, который определен в пространстве имен Microsoft . AspNetCore . Identi ty: using System . Threading.Tasks ; namespace Microsoft . AspNetCore . Identity puЬlic interface IUserValidator<TUser> where TUser : class { Ta sk<Identit yRes u l t > ValidateAsync(UserManager<TUser> manager , TUser user); Метод ValidateAsync () вызывается для выполнения проверки. В результате воз­ вращается объект того же самого класса Identi tyResul t . который применялся при проверке паролей. Чтобы продемонстрировать работу специального масса проверки, добавьте в папку Infrastructure файл класса по имени CustomUserValidator . cs с определением, представленным в листинге 28.26. Листинг 28.26. Содержимое файла CustomUserValidator. cs из папки Infrastructure us i ng System . Threading . Tasks; using Microsoft.AspNe tCore . Ident i ty ; using Users.Mode l s ; namespace Users . Infrastr u c ture { puЬlic class CustomUserValidat or : IU serValidator<AppUser> { puЬlic Task<IdentityResult> Va lidateAsync(UserManager<AppUser> rnanager , AppUser user) { if (user . Email . ToLower() . EndsWith( " @exarnple . com " )) return Task .FrornResult(IdentityResult . Success) ; else { return Task . FrornResult(IdentityResult . Failed(new IdentityError Code = "E rnailDornainError ", Description = " Only exarnple.com emai l addresses are allowed " })); Класс Cus t orn Us erValidator проверяет домен адреса электронной почты, чтобы удостовериться в том, что он является частью домена examp le . com . В листинге 28.27 специальный класс регистрируется как средство проверки для объе1пов AppUser . Листинг 28.27. Регистрация специального класса проверки пользователей в файле Startup. cs puЫic void ConfigureServices( I ServiceCol l ection services) services.AddTransient<IPasswordValidator<AppUser> , CustomPasswordValidator>() ; services.AddTransient<IUserValidator<AppUser>, CustomUserValidator>();
Глава 28. Введение в ASP.NET Соге ldentity 889 services.AddDbContext<AppidentityDbContext>(options => options.UseSqlServer( Configuration[ "D ata : SportStoreidentity : ConnectionString " ])) ; services.Addidentity<AppUser, IdentityRole>(opts => { opts.User . RequireUniqueEmail = true; opts.User . AllowedUserNameCharacters = "ab cdefghijklmnopqrstuvwxyz "; opts . Password . RequiredLength = 6 ; opts.Password . RequireNonAlphanumeric = false; opts . Password .RequireLowercase = false; opts.Password . RequireUppercase = false; opts .Password .RequireDigit = false; }) .AddEnt i tyFrameworkStores<AppidentityDbContext>() ; services.AddMvc(); Для тестирования специального IOiacca проверки запустите приложение и запол­ ните форму. возвращаемую для URL вида /Admin/Create, данными из табл . 28.12 . Таблица 28.12. Значения для создания примера пользователя Имя Name Email Password Описание ЬоЬ bob@invalid . com secret Имя пользователя и пароль успешно проходят проверку. но адрес электронной почты не относится к корректному домену. В результате отправки формы отобра­ зится сообщение об ошибке проверки (рис. 28.11) . 1 +- ;".! С . D localhost:S3221/Ac!mio/Create • Only example.com email addresses are allowed Nam e ЬоЬ ,,_...... ,~ ~.,.... ""..,,. ~~. Рис. 28.11 . Выполнение специальной проверки пользователей
890 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Процесс сочетания встроенной проверки, обеспечиваемой классом Us erValidator<T>, и специальной проверки аналогичен такому процессу для проверки паролей (листинг 28.28) . Листинг 28.28. Расширение встроенного класса проверки пользователей в файле CustomUserValidator. cs using System.Collections.Generic; using System.Linq; using System . Threading . Tasks ; using Mi crosoft .AspNetCore .I dentity ; using Users . Models; namespace Users .I nfrastructure { puЬlic class CustornUserValidator : UserValidator<AppUser> { puЫic override async Task<IdentityResult> ValidateAsync( UserManager<AppUser> manager , AppUser user) { IdentityResult result = await base.ValidateAsync(manager, user); List<IdentityError> errors = result.Succeeded? new List<IdentityError>() : result.Errors.ToList(); if (!user.Email .T oLower().EndsWith( " @example . com " )) errors . Add(new IdentityError { Code = "Em ailDomainError ", Description = " Only example. сот email addresses are allowed " }); return errors.Count ==О? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()); Завершение средств администрирования Для завершения инструмента администрирования осталось только реализовать средства для редактирования и удаления пользователей. В листинге 28 .29 показа ­ ны изменения, внесенные в файл Views/Admin/Index . cshtml для нацеливания на действия Edit и Delete контроллера Admin. Листинг 26.29. Добавление кнопок редактирования и удаления в файле Index.cshtml из папки Views/Admin @model IEnumeraЫe<AppUser> <div class= " bg-primary panel - body " ><h4>User Accounts</h4></div> <div class="text-danger" asp-validation-summary="ModelOnly"></div> <tаЫе class= " taЫe taЬle - condensed taЬle - bordered " > <tr><th>ID</th><th>Name</th><th>Email</th></tr> @if (Model . Count() == 0) { <tr><td colspan= " З " class= " text - center " >No User Accounts</td></tr>
Глава 28 . Введение в ASP.NET Саге ldentity 891 else { foreach (AppUser user in Model) { <tr> <td>@user . Id</td><td>@user .Use rName</td><td>@user . Email</td> <td> <form asp-action="Delete" asp-route-id="@user.Id" method="post"> <а class="Ьtn Ьtn-sm Ьtn-primary" asp-action="Edit" asp-route-id="@user.Id">Edit</a> <Ьutton type="suЬmit" </form> </td> </tr> </tаЫе> class="ьtn ьtn-sm ьtn-danger">Delete</button> <а class= " Ьtn Ьtn-primary" asp - action= " Create " >Create</a> Кнопка Delete (Удалить) отправляет форму действию Delete контроллера Admin, что важно, поскольку при изменении состояния приложения требуется за прос POST. Кнопка Edit (Редактировать) является якорным элементом, который будет отправлять запрос GET , т.к. первый шаг в процессе редактирования предусматривает отображе­ ние текуших данных. Кнопка Edit содержится в элементе form, поэтому СSS-стили Bootstrap не укладывают ее вертикально. Кроме того, в представление добавлена сводка по проверке достоверности модели, так что можно легко отображать сообщения об ошибках, которые поступают из ос­ тальных средств администрирования. Реализация средства удаления В классе UserManager<T> определен метод DeleteAsync (), который принимает экземпляр класса пользователя и удаляет его из базы данных. В листинге 28.30 ме­ тод DeleteAsync () используется для реализации средства удаления в контроллере Admin. Листинг 28.ЗО. Удаление пользователей в файле AdminCon troller. cs using Microsoft . AspNetCore . Identity; using Microsoft . AspNetCore . Mvc ; using Users . Models ; using System.Threading . Tasks; namespace Users . Contro l lers { puЫic class AdminController : Controller { private UserManager<AppUser> userManag er ; puЬlic AdminController(UserManager<AppUser> usrMgr) { userManager = usrMgr; / / ... для краткости другие действия не показаны ... [HttpPost] puЫic async Task<IActionResult> Delete (string id) AppUser user = await userМanager.FindВyidAsync(id);
892 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC if (user != null) { IdentityResult result = await userмanager.DeleteAsync(user); if (result.Succeeded) { return RedirectT0Action( 11 Index"); }else{ AddErrorsFromResult(result); } else { ModelState. AddМodelError ( 11 11 , 11 User Not Found11 ) ; return View ( 11 Index 11 , userManager. Users) ; private void AddErrorsFromResult(IdentityResult result) foreach (IdentityError error in result.Errors) { ModelState.AddМodelError( 1111 , error.Description); Метод действия Delete () получает в своем аргументе уникальный идентифика­ тор пользователя и применяет метод FindByi dAsyn c () для нахождения соответс ­ твующего объекта пользователя. который можно передать методу Dele t eAsync () . В качестве результата метод Delet eA s y n c () возвращает объект I dent i tyResult , который обрабатывается таким же образом, как в предшествующих примерах, чтобы обеспечить отображени е пользователю сообщений о любых ошибках. Для тестирова­ ния функциональности удаления создайте нового пользователя и затем щелкните на кнопке Delete, располож енной рядом с отображаемыми деталями об этом пользов ате­ ле в представлении Index (рис. 28 . 12). l~ u,..,. J~ с [Ф ~~~~~~~1n -.~- ----- ·- -----------·--' г-·- ---- ----- -·--- --- ---------- Name Emai l jID j 45Ьdf470-Оd6З-441 5·809е-dЬВ9Зсбd7еЫ le•tuse r test eexample.com i 1111 ! Ь908Ь741-7аа5-4е70-9367-06dа226467ЗЬ AI~ 1 1ID Na~ i Ь90ВЬ74f-7аа5 ·4е70-9З67-06dа226467ЗЬ Alice 1 еаЬСd12 1-ЗЬЗб-4fа2-а298-еdЗd857а3485 Joe alicelij)example.cшn 18118 i еаЬсd121 -ЗЬЗ6-4Jа2-а298-еdЗd857аЗ485 Joe( joet't exampJecom "" [ ! " ) !1111 i l---- ----- 1: _________________, - --- ----------·--- ------- ---- _ _j Рис. 28.12. Удаление пользовательской учетной записи Реализация возможности редактирования Для завершения инструмента администрирования необходимо добавить подде­ ржку редактирования адреса электронной почты и пароля, которые связаны с поль­ зовательской учетной записью.
Глава 28. Введение в ASP. NET Core ldentity 893 В настоящий момент имеются только свойства, определяемые пользователями , но в главе 30 будет показано. как расширить схему специальными свойствами. В листин­ ге 28.31 приведен код методов действий Edit () ,добавленных в контроллер Adm i n . Листинг 28.31. Добавление действий Edi t в файле AdminController. cs using Mi crosoft . AspNetCore . Identity ; using Mi crosoft . AspN e tCore . Mvc ; using Users . Models ; using System. Threading .T asks ; namespace Users.Con t rollers { puЬlic class Admi nControl ler : Controll er { private UserManager<Ap p User> u se rManag e r; private IUserValidator<AppUser> userValidator; private IPasswordValidator<AppUser> passwordValidator; private IPasswordНasher<AppUser> passwordНasher; puЫic AdminController(UserМanager<AppUser> usrMgr, IUserValidator<AppUser> userValid, IPasswordValidator<AppUser> passValid, IPasswordHasher<AppUser> passwordHash) userМanager = usrMgr; userValidator = userValid; passwordValidator = passValid; passwordHasher = passwordHash; 11 . . . для краткости другие действия не показаны . .. puЫic async Task<IActionResult> Edit(string id) { AppUser user = await userМanager.FindВyidAsync(id); if (user != null) { return View(user); else { return RedirectToAction("Index"); } [HttpPostJ puЫic async Task<IActionResult> Edit(string id, string email, st:r:ing password) { AppUser user = await userManager.FindВyidAsync(id); if (user != null) { user.Email = email; IdentityResult validEmail = await userValidator.ValidateAsync(userМanager, user); if (!validEmail.Succeeded) { AddErrorsFromResult(validEmail); Identi tyResul t validPass = null; if (!string.IsNullOrEmpty(password)) validPass = await passwordValidator.ValidateAsync(userМanager, user, password); if (validPass.Succeeded) { user.PasswordНash = passwordНasher.HashPassword(user, password) ;
894 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC }else{ Add.ErrorsFromResult(validPass); if ( (valid.Email. Succeeded && validPass == null) 11 (validEmail.Succeeded && password ! = string. Empty && validPass. Succeeded)) IdentityResult result = await userМanager.UpdateAsync(user); if (result.Succeeded) { return RedirectToAction("Index"); }else{ Add.ErrorsFromResult(result); else ModelState.AddМodelError("", "User Not Found"); } return View(user); private void AddErrorsFromResult(IdentityResult result) foreach (IdentityError error in result.Errors) { ModelState .AddModelErr or( "", error . Description) ; Действие Edi t, на которое нацелены запросы GET, использует строку идентифи­ катора, встроенную в представление Index, чтобы вызвать метод FindByidAsync () для получения объекта AppUser, представляющего пользователя. Более сложная реа­ лизация действия Edi t получает запрос POST и имеет аргументы для идентифш,ато­ ра пользователя, нового адреса электронной почты и пароля. Завершение операции редактирования требует выполнения нескольких задач. Первая задача связана с проверкой полученных значений. В настоящий момент работа ведется с простым объектом пользователя (хотя в главе 30 объясняется, как на­ страивать данные, сохраняемые для пользователей), но даже в этом случае нужно про­ верить пользовательские данные, чтобы обеспечить соблюдение специальных политик, которые были определены в разделах "Проверка паролей" и ··проверка деталей, связан­ ньrх с пользователем" ранее в главе. Сначала проверяется адрес электронной почты: user.Email = email; IdentityResult validEmail = await userValidator. ValidateAsync(userмanager, user); if ( ! validEmail . Succeeded) { AddErrorsFromResu lt (validEma il ) ; Кконструкторуконтроллерадобавленазависимостьот IUserValidator<AppUser>, чтобы можно было проверять новый адрес электронной почты. Обратите внимание , что перед выполнением проверки значение свойства Email должно быть изменено, т.к. метод ValidateAsync () принимает только экземпляры класса пользователя.
Глава 28. Введение в ASP. NET Core ldentity 895 Вторая задача связана с изменением пароля, если он был предоставлен. Система ASP.NET Core Identity сохраняет хеши паролей, а не сами пароли . Целью является пре­ пятствование похищению паролей. Следующая задача заключается в получении про ­ веренного пароля и генерации хеш-кода. который будет сохранен в базе данных. так что пользователь может быть аутентифицирован, как демонстрируется в главе 29. Паролипреобразуютсявхешичерезреализациюинтерфейса IРаsswоrdНаshеr<АррUsеr> , которая получается за счет объявления зависимости конструктора, распознаваемой посредством внедрения зависимостей. В интерфейсе IPasswordHasher определен метод HashPassword () , который принимает строковый аргумент и возвращает его хешированное значение : if (!string . IsNullOrEmpty(password)) validPass = await passwordValidator.ValidateAsync(userМanager, user, password) ; if (validPass . Succeeded) { user.PasswordHash = passwordHasher.HashPassword(user, password); else { AddErrorsF r omResu l t(validPass) ; Изменения , внесенные в класс пользователя, не сохраняются в базе данных до тех пор. пока не будет вызван метод UpdateAsync () : if ((validEmail . Succeeded && va lidPas s == nu ll ) 11 (validEmail . Succeeded && password != string .Ernpty && validPass .Succeeded)) { IdentityResult result = await userМanager.UpdateAsync(user); if (result . Succeeded) { return RedirectToAction( "Index" ) ; else { AddErrorsFrornResult(result) ; Соэдание представления Финальным компонентом является представление, которое будет отображать теку­ щие значения для пользователя и позволит отправлять контроллеру новые значения. Добавьте в папку Views/Adrnin файл по имени Edi t. cshtml с содержимым, приве­ денным в листинге 28.32. Листинг 28.32. Содержимое файла Edi t. cshtml из папки Views/Admin @rnodel AppUser <div class= " bg - prirnary panel - body " ><h4>Ed i t Use r </h4></div> <div asp - validation-surnrnary= "Al l " c las s =" text - danger " ></div> <forrn asp - action= " Edit " rnethod= "post " > <div class= " form - group " > <label asp- for= "Id"></l abel> <input asp-for= " Id " class= " form -contro l" d is aЫed /> </div>
896 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC <div class="form-group"> < l abel asp - for= " Ema il" ></label> < input asp-for="Email" c lass="form-con trol " /> </di v> <div class= " form - group " > < l abe l for= " password " >Password</ l abe l > <inp ut name = "p asswo rd" c las s= "form-contr o l" /> </di v> <b utt on type="submit " class= " btn Ьtn-primary">Save</button> <а asp-action="Index" class= " Ьtn Ьtn-default">Cancel</a> </form> Представление отображает идентификатор пользователя. который не может быть изменен , как статический текст и предлагает форму редактирования адреса элект­ ронной почты и пароля (рис . 28.13). Обратите внимание, что для элементов пароля дескрипторный вспомогательный класс не применяется, т.к. класс пользователя не содержит информации о пароле, поскольку в базе данных хранятся только хеширо­ ванные значения. ----·~·-·--'"'---·----"---·~~~---.....;.;:.,_·-·---··--·--·---·-~ __ ._ . ._ -_ ..__ .. ld 1 f26621ЗО-З26З4се7-Ьс4а·З4dсЗd663715 Emai l joe~example.com Password Cancel Рис. 28.1 З. Редактирование пользовательской учетной записи Последнее изменение связано с помещением в комментарий настроек проверки пользователей в классе Startup, чтобы для имен пользователей использовались стан­ дартные символы (листинг 28.33). Ввиду того, что некоторые учетные записи в базе данных были созданы до изменения настроек проверки. отредактировать их не удаст ­ ся, т.к. имена пользователей не пройдут проверку. А поскольку проверка применяется ко всему объекту пользователя , когда проверяется адрес электронной почты, резуль­ татом является пользовательская учетная запись, которую невозможно изменить.
Глава 28. Введение в ASP.NET Core ldentity 897 Листинг 28.ЗЗ. Комментирование настроек проверки пользователей в файле Startup. cs services . Addldentity<AppUser , IdentityRol e>(opts => { opts . Password.RequiredLength = 6 ; opts.Password . RequireNon LetterOrDigit = false ; opts.Password . Require Lowercase = fa l se ; opts.Password . RequireUppercase = false ; opts . Password . RequireDigi t = false ; opts . User . RequireUniqueEmail = true; 11 opts. User .AllowedUserNameCharacters = "aЬcdefghijklmnopqrstuvwxyz"; }) .AddEntityFrameworkStores<Appiden t ityDbCo nt ext>() ; Чтобы протестировать средство редактирования , запустите приложение, запроси­ те URL вида / Admin и щелкните на одной из кнопок Edit. Изм ените адрес электронной почты или введите новый пароль (л ибо сделайте то и другое), после чего щелкните на 1шопке Save (Сохранить) для обновления базы данных и возвращения к URL вида /Admin . Резюме В главе было показано, как создавать конфигурацию и классы , требующиеся для использования системы ASP.NET Core ldentity, а также прод е монстрировано , каким образом их можно применять для создания инструмента администрирования поль­ зователей. В следующей главе вы узнаете, как выполнять аутентификацию и автори­ зацию с помощью ASP.NET Core ld entity.
ГЛАВА 29 Применение ASP.NET Core Identity в этой главе будет показано, какприменять систему ASP.NET Core Identity для ау­ тентификации и авторизации пользовательских учетных записей, созданных в предыдущей глав е. В табл. 29.1 приведена сводка для настоящей главы. Таблица 29.1. Сводка по главе Задача Ограничение доступа к методу действия Решение Применяйте атрибут Authorize Листинг 29.1 Аутентификация пользователей Создайте контроллер Account, которы й полу- 29 .2-29.5 Создание и управление ролями Авторизация доступа к действию Обеспечение наличия учет­ ной записи администратора чает пользовательские учетные данные и прове- ряет их с использованием класса UserManager Используйте класс RoleManager Добавьте пользовательские учетные записи к ролям и применяйте атрибут Au thorize для указания, какие роли могут иметь доступ к ме­ тодам действий Поместите в базу данных начальные данные для автоматичес кого создания учетной записи Подготовка проекта для примера 29.6 -29.10 29.11-29 .18 29 .19-29.23 В главе будет продолжена работа с проектом Users, созданным в главе 28. В качес ­ тве подготовительных шагов запустите приложение, перейдит е на URL вида /Admin и, щелкая на кнопке Create (Со здать) , добавьте в базу данных по льзовательские учет ­ ные запи си из табл. 29.2 . Таблица 29.2 . Пользовательские учетные записи, требующиеся для настоящей главы Имя пользователя Адрес электронной почты Пароль Joe joe@example . com secret123 Alice alice@example.com secret123 ВоЬ bob@example . com secret123
Глава 29. Применение ASP.NEТ Соге ldentity 899 По завершении запрос URL вида / Ad.min должен привести к отображению списка пользователей, включая описанные в табл. 29.2 (не имеет значения, если вы создади­ те дополнительных пользователей; важно, чтобы присутствовали пользователи, ука­ занные в таблице), как показано на рис. 29.1 . 1· Use rs Х ,...---- -- --· -·-----·-----·--------·---·-----, ! 1_!"~1:· С · [') loc~lhost:625.::_:/дd_n~~~-..:...;;:...:......~·-·-·-·".~-~;::."..:..... .... .=. ..." -- ·- -- · --."~ . 'CJ·.•::_j 1 ID 1 1 4fc1 f959-Ыc8-4ede-ad41-6bc4d6953011 d4ca5981 -Зf2c-4Cff-83ee-75cOeb2919a3 1 f4d948e3-888c-4a3c-90f4-1 Зc4f8211324 ,., Name ВоЬ Joe Alice L.~~~~~-~~- Email bob @example.com joe @example.com alice @example.com Рис. 29.1. Запуск примера приложения Аутентификация пользователей • • _J Наиболее фундаментальной работой для системы ASP.NET Core ldentity является аутентификация пользователей. Основной инструмент для ограничения доступа I< методам действий - атрибут Authorize, который сообщает инфраструктуре МVС о том , что обрабатываться должны только запросы от аутентифицированных пользова­ телей. В листинге 29.1 атрибут Authorize применяется к действию Index контрол­ лера Home. Листинг 29.1 . Ограничение доступа в файле HomeController. cs using System.Collections . Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; namespace Users . Controllers ( puЫic class HomeController [Authorize] puЫic ViewResul t Index () => Controller ( View(new Dictionary<string , object> { ["Placeholder"J "Placeholder" }} ;
900 Часть 11 . Подробные сведения об инфраструктуре ASP. NET Core MVC После запуска приложения браузер отправит запрос на стандартный URL, кото­ рый будет нацелен на метод действия, декорированный атрибутом Au t horize. Пока что у пользователей нет никакой возможности аутентифицировать себя, поэтому в результате возникает ошибка (рис. 29.2). ~-2._ С ' [J localhost:62S4YAc~ount/=~$!~et_~~U1·f= ~:.::_..:~ ::__ 1 Status Code: 404; Not Found ....._____________J Рис. 29.2 . Нацеливание на защищенный метод действия Атрибут Authorize не указывает, как пользователь должен быть аутентифициро­ ван, и не имеет прямой ссылки на ASP.NET Core Identity. Службы ldentity и промежу­ точное ПО охватывают всю платформу ASP.NET, что делает их интеграцию в прило ­ жения МVС простой и бесшовной. Работа производится путем модификации объектов контекста, которые описывают НТТР-запросы, и снабжения МVС результатом процес­ са аутентификации без необходимости в предоставлении любых других деталей. Платформа ASP.NET предоставляет информацию о пользователе через объект HttpContext, который используется атрибутом Authorize для пров е рки состояния текущего запроса и выяснения, был ли пользователь аутентифицирован. Свойство HttpContext . User возвращает реализацию интерфейса IPrincipal, который оп­ ределен в пространстве имен System. Security. Principal. Интерфейс IPrincipal определяет свойство и метод, показанные в табл. 29.3 . Таблица 29.3 . Избранные члены, определяемые интерфейсом IPrincipal Имя Identity IsinRole(role) Описание Возвращает реализацию интерфейса IIdentity, который описывает пользователя, ассоциированного с запросом Возвращает true, если пользователь является членом указанной роли. Управление авторизацией с помощью ролей объясняется в раз­ деле "Авторизация пользователей с помощью ролей" далее в главе Реализация интерфейса IIdentity, возвращаемая свойством IPrincipal . Identi ty, предлагает базовую, но полезную информацию о текущем пользователе через свойства, которые описаны в табл. 29.4 . Совет. В главе 30 рассматривается класс реализации по имени Claimsidenti ty, который ASP.NET Core ldentity применяет для интерфейса IIdentity.
Глава 29. Применение ASP.NET Core ldentity 901 Таблица 29.4 . Избранные свойства, определяемые интерфейсом IIdenti ty Имя AuthenticationType IsAuthe nticated Name Описание Возвращает строку, которая описывает механизм, используемый для аутентификации пользователя Возвращает t r u e, если пользователь был аутентифицирован Возвращает имя текущего пользователя Промежуточное ПО ASP.NET Core Identity применяет сооkiе-наборы. посылаемые браузером, для выяснения, был ли пользователь аутентифицирован. Если пользова­ тель прошел аутентификацию, тогда свойство IIde n t i ty. I sAuth en t ica ted уста­ навливается в true . Поскольку пример приложения пока не располагает механизмом аутентификации, свойство I s Au thenticated всегда возвращает fa lse , что приво­ дит к ошибке аутентификации. В результате клиент перенаправляется на URL вида /Accoun t/Login , который является стандартным URL для предоставления учетных данных аутентиф икации. Браузер запрашивает URL вида /Acc o unt / Log i n , но из-за того, что в проекте он не соответствует какому-либо контроллеру или действию , сервер возвращает ответ 404 - No t Fo u nd (404 - не найдено). давая в итоге сообщение об ошибке, показанное на рис . 29.2. Изменение URL для входа Хотя /Account/Login - это стандартный URL, на который клиенты перенаправляются, когда требуется авторизация, в методе Conf igu r e Servi ces () класса Start u p можно указать собственный URL, изменив параметр конфигурации при настройке служб ASP.NET Core ldeпtity: services.Addi de nt i ty<AppUs e r, IdentityRole>( opts => ( opts.Cookies.ApplicationCookie.LoginPath = "/Users/Login"; }) .A d dE nt i t yFramewor kSt ores <Appident ityDbCo ntext>() ; Система ldeпtity не может полагаться на систему маршрутизации при генерации своих URL, так что цель перенаправления должна указываться буквально. В случае изменения схемы маршрутизации, используемой приложением, потребуется также обеспечить изменение на­ стройки ldeпtity, чтобы URL по-прежнему достигал целевого контроллера. Подготовка к реализации аутентификации Несмотря на то что запрос заканчивается выводом сообщения об ошибке, он ил­ люстрирует, каким обр азом система ASP.NET Core Identity вписывается в стандартный жизненный цикл запросов ASP.NET . Следующий шаг заключается в реализации. конт­ роллера. который будет получать запросы для URL вида /Accoun t / Log i n и аутенти­ фицировать пользователя. Добавьте новый класс модели в файл UserViewMode l s . cs (листинг 29.2).
902 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Листинг 29.2 . Добавление нового класса модели в файле UserViewModels. cs using System . ComponentModel . DataAnnotat i ons ; namespace Users.Models { puЫic class CreateModel [Required ] puЫic string Name { get ; set; } [Required ] puЫic string Email { get ; set ; } [Required ] puЬlic string Password { get ; set; puЫic class LoginМodel [Required] [UIHint("email")] puЬlic string Email get; set; } [Required] [UIHint("password")] puЬlic string Password { get; set; } Новый класс модели имеет свойства Email и Password, декорированные атр и­ бутом Required, так что можно применять проверку достоверности моделей для контроля, предоставил ли пользователь значения. Свойства также декорированы атрибутом UIHint. который гарантирует. что элементы input, ви зуализируемые де ­ скрипторным вспомогательным классом в представлении, будут иметь соответствую­ щим образом установленные атрибуты t уре . Совет. В реальном проекте для выяснения, предоставил ли пользователь значения для име­ ни и пароля, перед отправкой формы серверу можно использовать проверку достовер­ ности на стороне клиента, которая была описана в главе 27 . Добавьте в папку Controllers файл класса по имени AccountController . cs и поместите в него определ ение контроллера, приведенное в листинге 29.3 . Листинг 29.3. Содержимое файла AccountController. cs из папки Controllers using System . Threading .T asks ; using Microsoft . AspNetCore . Au tho ri zation ; using Microsoft . AspNetCore . Mvc ; using Users.Models; namespace Users.Controllers [Authorize] puЬlic c l ass AccountController Controller { [AllowAnonymous]
Глава 29 . Применение ASP.NET Core ldentity 903 puЫic IActionResult Login(string returnUrl) { ViewBag.returnUrl = returnUrl; return View(); [HttpPost ] [Al l owAnonymous] [ValidateAntiForgeryToken] puЫic async Task<IActionResult> Login(LoginModel details, string returnUrl) { return View(details); В листинге 29.3 логика аутентификации не бьmа реализована, потому что плани­ руется определить представление и затем пройти через процесс проверки пользова­ тельских учетных данных и входа пользователей в приложение. Хотя контроллер Accoun t пока еще не аутентифицирует пользователей, он содер­ жит удобную инфраструктуру. заслуживающую объяснения отдельно от кода ASP.NET Соге Identity, который вскоре будет добавлен в метод действия Log in ( ) . Первым делом обратите внимание, что обе версии метода действия Log in ( ) при­ нимают аргумент по имени returnUrl. Когда пользователь запрашивает ограничен­ ный URL, он перенаправляется на URL вида /Account/Login со строкой запроса, в которой указан URL, куда пользователь должен быть направлен после того, как он успешно пройдет аутентификацию. Удостовериться в этом можно, запустив приложе­ ние и запросив URL вида /Horne/Index. Браузер будет перенаправлен примерно так: /Account/Login?ReturnUrl= %2FHome %2Findex Значение параметра ReturnUrl строки запроса делает возможным такое перена­ правление пользователя, что навигация междУ открытыми и защищенными частями приложения превращается в простой и гладкий процесс. Далее обратите внимание на атрибуты, которые были применены в контроллере Account. Контроллеры, управляющие пользовательскими учетными записями, содер­ жат функциональность, которая должна быть доступна только аутентифицированным пользователям, такую как сброс пароля, например. С этой целью к классу контролле­ ра был применен атрибут Authorize, а к индивидУальным методам действий - ат­ рибут Al l owAnonymous . В итоге доступ к методам действий по умолчанию ограничивается аутентифицирован­ ными пользователями, но пользователям, не прошедшим аутентификацию, разрешено входить в приложение. Кроме того, применяется атрибут ValidateAntiForgeryToken, описанный в главе 24, который работает в сочетании с дескрипторным вспомогатель­ ным классом для элемента forrn в целях противодействия подделке межсайто вых запросов. Последний подготовительный шаг связан с созданием представления, которое бу­ дет визуализироваться для сбора учетных данных от пользователя. Создайте папку Views/Account и добавьте в нее файл представления по имени Login. cshtrnl с раз­ меткой из листинга 29.4 .
904 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 29.4 . Содержимое файла Login. cshtml из папки Views/Account @model Log i nMode l <d i v c lass=" bg-pri ma r y panel-body "><h4 >Log In</h4></di v> <div c l ass= " text-danger " as p -val i dation- s u mrnary= " Al l" ></div > <f orm asp- action= " Login" method=" post"> <input type="hidden" name="ret urnUrl" val ue= " @ViewBag.returnUrl" /> <div cl ass=" form-group"> <l abel asp- for = " Email"></label> <input asp-for="Email" cl ass="form- cont rol" /> </div> <di v cl ass=" form- group"> < l abel asp -for= " Password " ></ l abel> <i nput asp- for= " Password " class= " form- con trol" /> </di v> <button class="btn btn- primary" type=" s ubmi t ">Log In</ button> </form> Единственный примечательный асп е кт данного пр едстав л ения - скрытый эле­ мент i n put , который сохраняет аргумент retu r n Ur l . Во всех остальных отношениях это стандартное представление Razor, но оно завершает подготовку к аутентифика­ ции и демонстрирует способ перехвата и перенаправления запросов , не прошедших аутентификацию. Чтобы протестировать новый контролл е р, запустите прило жени е. Когда браузер запрашивает стандартный URL приложения, он пер енаправляется на URL вида /Accoun t / L ogin , что выдает содержим ое, показанное на р и с. 29.3 . 1 u"" х ~ -:i С :D-lo ca lh-;; st :62S42iAccou~tfl-;g i11?R-;,t~~ 11U rJ :% 2FHom; %2 Flnd-;,-;---~: S 1 ---...:.::.·--~-'"---·."~::.....::..._..___.".. .._ _ .":..::."'·-· "·---~--~-·-·--1 Emai l 1 Passwo rd ~-------------------·· J - Рис. 29.З. Вывод пользователю приглашения предоставить свои учетные данные Добавление аутентификации пользо вателей Запросы к защищенным методам действий корректно перенаправляются контрол­ леру Acco u n t, но учетные данные , предоставляемы е пользователем , пока ещ е не ис-
Глава 29 . Применение ASP.NET Соге ldentity 905 пользуются для аутентификации. В листинге 29.5 завершается реализация действия Login за счет применения служб ASP.NET Core Ideпtity для аутентификации пользо­ вателя с участием деталей, хранящихся в базе данных. Листинг 29.5 . Добавление аутентификации в файле AccountController. cs using System . Threading .Tasks ; using Microsoft . AspNetCore . Authorization ; using Microsoft . AspNetCore.Mvc ; using Users . Models ; using Microsoft.AspNetCore.Identity; namespace Users . Controllers { [Authorize] puЫic class AccountController : Cont roller { private UserManager<AppUser> userManager; private SigninМanager<AppUser> signinМanager; puЬlic AccountController(Userмanager<AppUser> userMgr, SigninМanager<AppUser> signinМgr) { userмanager = userMgr; signinМanager = signinМgr; [AllowAnonymous] puЫic IActionResult Login(string returnUrl ) { ViewBag.returnUrl = returnUrl ; return View () ; [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] puЫic async Task<IActionResult> Login(LoginМodel details, string returnUrl) { if (ModelState . IsValid) { AppUser user = await userManager.FindВyEmailAsync(details.Email); if (user != null) { await signinМanager.SignOutAsync(); Microsoft.AspNetCore.Identity.SigninResult result await signinмanager.PasswordSigninAsync( user, details.Password, false, false); if (result.Succeeded) { return Redirect (returnUrl ? ? "/ ") ; ModelState.AddМodelError(nameof (LoginМodel . Email), "Invalid user or password") ; return View(details); Простейшей частью является получение объекта AppUser, который представ­ ляет пользователя. что делается посредством метода FindByEmailAsync () класса UserManager<AppUser>: AppUser user = await userManager.FindВyEmailAsync(details.Email);
906 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC Метод FindByEmailAsync () находит пользовательскую учетную запись, исполь­ зуя адрес электронной почты, который применялся при ее создании. Имеются также альтернативные методы поиска по идентификатору. по имени и по входу. Адрес элек­ тронной почты используется для входа из-за того, что такой подход принят в боль­ шинстве веб-приложений, доступных через Интернет, и он набирает популярность также в корпоративных приложениях. В случае если учетная запись с указанным пользователем адресом электрон ­ ной почты существует, тогда производится аутентификация с прим енением класса SigninManager<AppUser>. для которого добавляется аргумент конструктора, рас­ познаваемый с помощью внедрения зависимостей . Класс SigninManager использу ­ ется для выполнения двух шагов аутентификации: await signinManage r.SignOutAsync () ; Microsoft.AspNetCore.Identity.SigninResult result = await signinManager .PasswordSigninAsync (use r, details.Password , false , false) ; Метод SignOutAsync () аннулирует любой имеющийся у пользователя сеанс, а метод PasswordSignin () проводит саму аутентификацию. В качестве аргументов метод PasswordSignlnAsync () получает объект пользователя, предоставленный пользователем пароль, булевское значение, управляющее постоянством сооkiе-набора аутентификации (отключ ено), и признак , должна ли учетная запись блокироваться в случае некорректного пароля (отключ ено) . Результатом метода PasswordSigninAsync () будет объект SigninResu l t, в котором определено булевское свойство Succeeded, указывающее на успешность аутентификации. В рассматриваемом примере проверяется свойство Su cce e ded ; если оно рав­ но true, то пользователь перенаправляется на местоположение returnUrl, а если false, тогда добавляется ошибка проверки достоверности и затем представление Login отображается заново, чтобы пользователь смог повторить попытку. Как часть процесса аутентификации система Identity добавляет к ответу сооkiе­ набор, который браузер затем включает в любые последующие запросы, чтобы иден­ тифицировать сеанс пользователя и ассоциированную с ним учетную запись. Вы не обязаны создавать или управлять зтим сооkiе-набором напрямую, т.к. он поддержи­ вается автоматически промежуточным ПО Identity. Учет двухфакторной аутентификации В настоящей главе выполнялась однофа кторная аутентификация, при которой пользователь может быть аутентифицирован с применением одиночной порции информации, известной ему заранее: пароля . Система ASP.NET Core ldentity поддер жи вает также двухфакторную аутентификацию, когда пользователю нужно кое-что дополнительное, обычно получаемое в момент, когда он желает пройти аутентификацию. Наиболее распространенными примерами могут быть значение из маркера SecurelD или код аутентификации, который отправляется в виде сообщения элек­ тронной почты или текстового сообщения. (Строго говоря, в качестве двух факторов может выступать что угодно , включая отпечатки пальцев , результаты сканирования радужной обо­ лочки глаз или распознавания голоса , хотя в большинстве веб-приложений такие варианты востребованы редко.)
Глава 29. Применение ASP.NET Соге ldentity 907 Защита в итоге усиливается, потому что злоумышленнику необходимо знать пароль пользо­ вателя и иметь доступ к тому, что предоставляется как второй фактор, наподобие учетной записи электронной почты или сотового телефона. Двухфакторная аутентификация в книге не рассматривается по двум причинам. Во-первых, она требует большой подготовительной работы, связанной с настройкой инфраструктуры, которая распространяет сообщения электронной почты и текстовые сообщения второго фактора, и реализации логики проверки, что выходит за рамки тематики этой книги. Во-вторых, двухфакторная аутентификация вынуждает пользователя помнить о необходи­ мости прохождения второго шага аутентификации, для чего держать поблизости, например, сотовый телефон или маркер безопасности, что в случае веб-приложений не всегда оказы­ вается подходящим. Я более десяти лет проносил с собой маркер SecurelD того или иного вида на различных работах и потерял счет, сколько раз не мог войти в систему работодате­ ля из-за того, что забывал маркер дома. Если вы заинтересованы в двухфакторной аутентификации, тогда рекомендуется опираться на стороннего поставщика вроде Google, который позволяет пользователю самостоятельно выбрать, желает ли он иметь дополнительную защиту (и терпеть неудобства), обеспечива­ емую двухфакторной аутентификацией. Использование сторонней аутентификации демонс­ трируется в главе 30. Тестирование аутентификации Чтобы протестировать аутентификацию пользователей, запустите приложение и запросите URL вида / Ho me /I n dex. После перенаправления на URL вида /Accoun t/ L og i n введите учетные данные одного из пользователей, перечисленных в начале главе (скажем. адрес электронной почты j o e @ex amp l e.c om и пароль se c retl 23). Щелкните на кнопке Log ln (Вход) и браузер будет перенаправлен обратно на /Home / Index, но на этот раз он отправит сооkiе-набор аутентификации , который предоста­ вит доступ к методу действия (рис. 29.4). Совет. Для просмотра сооkiе-наборов, применяемых при идентификации аутентифицирован­ ных запросов, можно использовать инструменты разработчика, встроенные в браузер. ( Us~" >< ,, +- -:' С D lo~h-os-t:б-'2==54""21""дc==co=,"~\1 Log-1n --- --------- '5' Е 1:------------· !•. ~~ ~;~<:.~-°--~~~~~~~-~------------~ !! 1 Email 1 joe@example.com 1 1 "'_:"' ~ "~'"""" ' """'"" __ __J 1• ] ~'--·----------------- Рис. 29.4 . Аутентификация пользователя
908 Часть//. Подробные сведения об инфраструктуре ASP.NET Соге MVC Авторизация пользователей с помощью ролей В предыдущем разделе атрибут Authorize применялся в самой базовой форме, которая позволяет любому аутентифицированному пользователю выполнять метод действия. Его также можно использовать для уточнения авторизации, чтобы полу­ чить более детальный контроль над тем, какие пользователи могут выполнять те или иные действия, основываясь на принадлежности пользователя к роли. Роль - это всего лишь произвольная метка, которая определяется для представле­ ния разрешения выполнять набор действий внутри приложения. Практически каж­ дое приложение проводит различие между пользователями, которые могут выполнять административные функции, и пользователями, которые не могут. В мире ролей это делается путем создания роли Administrat o rs и ее назначении пользователям. Пользователи могут принадлежать ко многим ролям, а связанные с ролями разреше­ ния могут быть настолько крупнозернистыми или мелкозернистыми, насколько это желательно. Таким образом, с применением разных ролей можно проводить различие между администраторами, которым позволено выполнять базовые задачи, подобные созданию новых учетных записей, и администраторами, которым разрешено выпол­ нять более I{ритичные операции вроде доступа к данным о платежах. Система ASP.NET Core Identity берет на себя ответственность за управление на­ бором ролей, определенных в приложении, и отслеживание членства пользователей в них. Но ей ничего не известно о том, что означает каждая роль; эта информация содержится внутри части MVC приложения, где доступ к методам действий ограни­ чивается на основе членства в ролях. Для доступа и управления ролями в ASP.NET Core ldentity предусмотрен строrо типизированный базовый класс по имени RoleManager<T>, где Т - нласс, 1юторый представляет роли в механизме хранения. Инфраструктура Entity Framework Core ис­ пользует для представления ролей класс Identi tyRole, в котором определены свойс­ тва, перечисленные в табл. 29.5 . Таблица 29.5. Избранные свойства класса Identi tyRole Имя Id Name Users Описание Определяет уникальный идентификатор для роли Определяет имя роли Возвращает коллекцию объектов Identit yUserRole, которые представляют члены роли При желании расширить встроенную функциональность, которая описана в главе 30 для объектов пользователей, можно создать нласс роли, специфичный для приложения, но здесь будет применяться класс IdentityRole, т.к. он делает все, в чем нуждается большинство приложений. Когда конфигурировалось приложение в главе 28, системе ASP.NET Core Identity уже было указано на необходимость исполь­ зования класса IdentityRole для представления ролей, что демонстрирует следую­ щий оператор в методе ConfigureServices () класса Startup: services.Addidentity<AppUser, IdentityRole>(opts => { opts . User . Require Un iqueEmail = true; 11 op t s . User.All owe dUs e rNameCharac t ers =
Глава 29. Применение ASP.NET Core ldentity 909 " abcdefghijklmnopqrstuvwxyz "; opts . Password.RequiredLength = 6 ; opts.Password . RequireNonAlphanumeric = false; opts.Password . RequireLowercase = false; opts . Password . RequireUppercase = false; opts . Password . RequireDigit = false; }) . AddEntityFrameworkStores<AppidentityDbContext>() ; Параметры типов в методе Addidenti ty () указывают классы , которые будут при­ меняться для представления пользователей и ролей. В примере приложения для пред­ ставления пользователей используется масс AppUser, а для представления ролей - встроенный класс Identi tyRole . Создание и удаление ролей Чтобы продемонстрировать прим енение ролей, мы создадим инструмент ад­ министрирования для управления ими, начав с методов действий, которые могут создавать и удалять роли. Добавьте в паш<у Controllers файл класса по имени RoleAdminController . cs и определите в нем контроллер, как показано в листинге 29.6. Листинг 29.6 . Содержимое файла RoleAdminController. cs из папки Controllers using System . ComponentModel.DataAnnotations ; using System . Threading . Tasks ; using Microsoft . AspNetCore . Identity ; using Microsoft . AspNetCore . Identity . EntityFr ameworkCore ; using Microsoft . AspNetCore . Mvc ; namespace Users . Controllers { puЫic class RoleAdminController : Cont r oller ( private RoleManager<IdentityRole> r o l eManager ; puЫic RoleAdminController(RoleManager<IdentityRole> roleMg r ) { roleManager = roleMgr; puЬlic ViewResult Index() => View(roleManager . Roles); puЫic IActionResult Create() => View() ; [HttpPost] puЫic async Task<IActionResult> Create( [ Required]string name) { if (ModelState. IsValid) { IdentityResult result = await roleManager . CreateAsync(new I dentityRole(name)) ; if (result . Succeeded) ( return RedirectToAction{ " Index " ) ; else { AddErrorsFromResult(result) ; return View{name) ; [HttpPost]
910 Часть 11 . Подробные сведения об инфрастру к туре ASP.NET Core MVC puЫic async Task< I Act i onResu l t> De l ete(s t ring id) { IdentityRole rol e = awa it rol eManager . FindByi dAsync(id) ; if (role ! =null) { IdentityResult result = await ro l eManager . De l eteAsync(role) ; i f (result.Succeeded) { return RedirectToAction( " Index " ) ; else { AddErrorsFromResult(result ) ; else { Mode l State . AddMode l Error( "", " No role foun d" ) ; return View( "I ndex ", roleManage r.Roles) ; priva t e void AddErrorsFromResult(IdentityResult res u lt) fo r each ( I dentity Error error in res u lt.Errors) { Mode l State . AddModelError( "", error . Description) ; Управлен ие ролями производится с использованием класса RoleManager< T>, где т -тип, предназначенный для пр едставления ролей (встроенный класс Identi tyRole в данном приложении) . Конструктор RoleAdminController объявля ет зависимость от RoleManager<IdentityRole>. которая распознается посред ством внедр ения за ­ висимостей при создании объекта контроллера. В класс е RoleManager<T> определены ме т оды и сво й ства , пер е численны е в табл . 29.6, которы е по зволяют со здавать и управлять ролями. Таблица 29.6 . Члены, определяемые классом RoleМanager<T> Имя Cre ate Async( r ole) DeleteAsync(role) FindByidAsync(id) FindByNameAsync( n ame) Ro l eEx i stsAs ync(name) UpdateAsync( r ole) Roles Описание Создает новую роль Удаляет указанную роль Находит роль по ее идентификатору Находит роль по ее имени Возвращает true, если роль с указанным именем существует Сохраняет изменения в указанной роли Возвращает перечисление ролей, которые были определены М етод де йствия Index () нового контроллера отобр аж ает вс е роли в пр ило жении . Метод действия Create ( ) применяется для отображения и получения формы , данные которой используются при создании новой роли с помощью метода Cr eateAsync (). Метод действия De l ete () получает запрос POST и принимает уникальный иденти­ фикатор роли , который применяется для е е удаления из приложения чер ез метод DeleteAsync (), находя объект роли с применением м етода FindByidAsync () .
Глава 29 . Применение ASP.NET Core ldentity 91 1 Создание пред ста влений Чтобы отобразить детали ролей в приложении, создайте папку Views/RoleAdmin и добавьте в нее файл Index. csht ml с разметкой из листинга 29.7 . Л исти нг 29.7 . С одержимое файла Index. cshtml из папки Views/RoleAdrnin @model IEnumeraЬle<Ident i tyRole> <div class= " bg - p r imary p anel - body " ><h4>Ro l es</h4></div> <div c l ass= " text - danger " asp - va l idation - summary=" Mode l Only " ></div> <tаЫе class= " taЫe taЬle - condensed taЬle - bordered taЬle - bordered " > <tr><th>ID</th><th>Name</th><th>Users </th><th></th></tr> @if (Model.Count() == 0) { <tr><td colspan =" 4 " class= " text - center " >No Roles</td></tr> else { foreach (var role in Mode l ) <tr> <td>@role . Id</td> <td>@role . Name</td> <td identity- role=" @rol e .Id"></t d> <td> <form asp- action=" Delete" asp- rout e- id= " @rol e . Id" method= "post"> <а class= " btn btn- sm btn- p r imary " asp-action=" Edit " asp - route - id=" @role . Id " >Edit</a> <button type="s ubmi t " class="Ьtn Ьtn-sm Ьtn-danger"> Delete </button> </form> </td> </tr> </tаЫе> <а class= " btn btn - pr imary " asp - act i on =" Create " >Create</a> Для отображения деталей о ролях в приложении представл ение использует табли ­ цу. В третьей колонке применяется специальный атрибут элемента: <td identity-role="@role. Id" ></ t d> Нужно отобра з ить список пользов ат ел е й. которы е являются членами каждо й роли, что требует включения в представление очень большого объема кода. Чтобы сохр анить представление простым, добавьте в папку I n f rast r uct u re файл класса по имени RoleUsersTagHelper . cs и по м естите в него опр еде лени е де скрипторного вспомогательного класса , приведенное в листинге 29.8 . Листинг 29.8. Содержимое файла RoleUsersTagHelper. cs и з папки Infrastructure using System.Collections . Gen e ric ; using System . Threading . Tasks ; using Microsoft . AspNetCore . Identity ;
912 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC using Microsoft . AspNetCore.Identity . EntityFrameworkCore ; using Microsoft.AspNetCore . Razor . TagHe l pers ; using Users.Model s; namespace Users . Infrastructure [HtmlTargetEl e ment( " td", At tributes = "identity- rol e " ) J puЫic c l ass Role UsersTagHelper : TagHelper { private UserManager<App User> u serManager ; private RoleManager< I dent i tyRole> roleManager ; puЬlic RoleUsersTagHelpe r (UserMa n ager<Ap pUser> usermgr , Ro l eManag er<I dentityRole > rolemgr ) { use rM anage r usermgr; rol eMa n ager = rolemgr; [Html AttributeName( " identity- ro l e " ) ] puЫicstringRole {get; set; } puЬlic override async Task ProcessAsync(TagHelperContext context , TagHel per Output o utput) { List<stri ng> names = new List<string>(); IdentityRo l e role = awa it roleManager .FindByidAsync(Role) ; if (role !=null ) { foreach (var user in userManager . Users) { if (user != null && await userManager .Isi nRoleAsync(us e r , role . Name)) { names . Add(user . Use r Name) ; output . Content . SetConte n t(names . Cou n t О? " No Users ": string . Join ( ", ", names)) ; Этот дескрипторный вспомогательный класс опер ирует на элементах t d поср едс ­ твом атрибута identi t y - role, который используется для получ е ния им е ни обраба­ тываемой роли. Объекты Ro l eManager<IdentityRole> и UserManager<AppUser> позволяют отправлять запросы базе данных Identlty для построения спи с к а и ме н пользователей в роли. В листинге 29.9 к файлу импортирования представл ений до ­ бавляется дескрипторный вспомогательный класс RoleUse r sTagHe l per и выраж е ­ ние @us i ng , чтобы на типы EF Core можно было ссылаться внутри пр едставлени й, не ук аз ывая пространство имен . Листинг 29.9 . Добавление дескрипторного вспомогательного класса в файле _ Viewimports . cshtml @using Users .Model s @using Microsoft.AspNetCore.Identity.EntityFrameworkCore @addTagHe l per * , Mi crosoft . Asp NetCore . Mvc .T agHe l pers @addTagHelper Users.Infrastructure.*, Users
Глава 29 . Применение ASP.NET Core ldentity 913 Добавьте в папку Views/RoleAdmin файл представления по имени Create . cshtml с разметкой из листинга 29.10 для поддержки добавления новых ролей. Листинг 29.1 О. Содержимое файла Create. cshtml из папки Views/Ro leAdmin @model strin g <div class= " bg - primary panel - body " ><h4>Create Ro l e</h 4></div> <div asp - validation- summary= "All" class="text-danger"></div> <form asp - action= "Create " method="post"> <div c l ass="form- group " > <label for= " name " ></label> <inp ut name="name" class= "form-cont rol " /> </div> <button type= " submit " c l ass= " btn btn-primary">Create</button> <а asp - action= " Index " class ="Ьtn Ьtn-default">Cancel</a> </form> Для создания роли из данных формы требуется только имя - вот почему в Create . cshtml имеется возможность применения string в качестве класса модели представ­ ления. Мы хотим воспользоваться преимуществами проверки достоверности модели. чтобы удостовериться в предоставлении пользователем значения, когда форма отправ­ лена, но для такой простой задачи не стоит создавать отдельный класс модели. Взглянув на метод Crea te () . принимающий запросы POST в листинге 29.6 . вы заметите, что атрибут проверки достоверности Required применяется прямо к параметру . Результат будет таким же, как в случае применения этого атрибута в классе модели , и появляется возможность задействовать встроенный процесс проверки достоверности моделей. Тестирование создания и удаления ролей Чтобы протестировать новый контроллер , запустите приложение и перейдите на URL вида /RoleAdmin. Щелкните на кнопке Create (Создать), введите имя в элементе input и щелкните на второй кнопке Create. Новая роль будет сохранена в базе дан­ ных и отображена после перенаправления браузера на действие Index (рис . 29.5) . Щелкнув на кнопке De lete (Удалить), роль можно удалить из приложения. 1О Name Users Ьае5а ОП-706Ь-407f- Ь89Ь·4170Ь4437ЬеЬ Admins No Users 111111 ccc2df5c-ЗC1e-4c6c-ae5d - 5db140с45а00 Users No Users ." l lEI L---------------------------~ Рис. 29.5. Создание новой ро л и
914 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Управление членством в ролях Следующий шаг - обеспечение возможности добавления и удаления пользовате­ лей из ролей. Сам процесс несложе н ; он предусматривает взя тие данных роли из клас­ са RoleManager и их ассоциирование с деталями индивидуальных пользователей. Для начала понадобится определить несколько классов моделей представлений, которы е будут представлять чле нство в роли и получать новый н абор инструкций от­ носительно членства от поль зователя. В листинге 29.11 показаны добавления, вне­ сенные в файл UserViewModels . cs из папки Models. Листинг 29.11 . Добавление моделей представлений в файле userViewModels. cs using System . ComponentModel . DataAnnotations ; using System . Collections.Generic; using Microsoft.AspNetCore.Identity.EntityFrameworkCore ; namespace Osers.Models { puЬlic class CreateModel [Required] puЫic string Name { get ; set ; } [Required] puЬlic string Email { get; set; } [Required] puЫic string Password { get; set ; puЬlic class LoginModel [Requi red] [UIHint( " email " )J puЫic string Email [Required] [UIHint ( " password " )] get; set; } puЫic string Password { get ; set ; } puЬlic class RoleEditмodel { puЫic IdentityRole Role { get; set; } puЬlic IEnumeraЫe<AppUser> MemЬers { get; set; } puЫic IEnumeraЫe<AppUser> NonМemЬers { get; set; puЫic class RoleModificationМodel { [Required] } puЬlic string RoleName { get; set; } puЬlic string Roleid { get; set; } puЫic string[J IdsToAdd { get; set; puЬlic s tring [] IdsToDelete { get; set; Кл асс RoleEdi tModel пр едставляет роль и детали о пользователях в системе, ка­ тегоризированные по членству в роли . Класс RoleModificationModel представля ет набор изменений роли . В листинге 29.12 к контроллеру RoleAdrnin добавляют ся но­ вые методы действий, которые используют модели представл ений из листинга 29 .11 для управления членством в ролях.
Глава 29. Применение ASP.NET Core ldentity 915 Л и стинг 29.12. Добавление методов действий в файле RoleAdminController. cs using System . ComponentModel . DataAn notations ; using System . Threadi ng . Tasks ; using Microsoft . AspNetCore . Identity ; using Microsoft . AspNetCore .I de n tity . E n tityFrameworkCore ; using Microsoft . AspNe t Core . Mvc ; using System.Linq; using Users.Models; using System.Collections.Generic; namespace Users . Control l ers { puЫic class RoleAdmi nControl l er : Control l er { private RoleManager<Ide n ti t yRo l e> roleManager ; private UserManager<AppUser> userManager; puЫic RoleAdminCont r o l ler{RoleManager< I de n ti t yRo l e> r o l eMgr , UserManager<AppUser> userMrg) { ro l eManager = roleMgr ; userManager = userMrg; 11 .. . для краткости другие методы действий не п оказаны ... puЫic async Task<IActionResult> Edit(string id) { IdentityRole role = await roleМanager.FindВyidAsync(id); List<AppUser> memЬers = new List<AppUser> () ; List<AppUser> nonМemЬers = new List<AppUser>(); foreach (AppUser user in userмanager.Users) { var list = await userManager.IsinRoleAsync(user, role.Name) ? memЬers : nonМemЬers; list.Add(user); return View (new RoleEditмodel Role = role, MemЬers = memЬers, NonМemЬ e rs = nonМemЬers }); [HttpPost] puЬlic async Task<IActionResult> Edit(RoleModificationМodel model) IdentityResult result; if (ModelState.IsValid) { foreach (string userid in model.IdsToAdd ?? new string[] { }) { AppUser user = await userManager.FindВyidAsync(userid); if (user != null) { result = await userManager.AddToRoleAsync(user, model . RoleName); if (!result.Succeeded) { AddErrorsFromResult(result); foreach (string userid in model . IdsToDelete ?? new string[] { }) { AppUser user = await userмanager.FindВyidAsync(userid);
916 Часть 11. Подро бные сведения об инфраструктуре ASP.NET Core MVC if (user != null) { result = await userManager.RemoveFromRoleAsync(user, model.RoleName); if (!result.Succeeded) { AddErrorsFromResult(result); } if (ModelState. IsValid) { return RedirectToAction(nameof(Index)); else { return awai t Edit (model. Roleid) ; private void AddErrorsFrornResult(IdentityResult result) foreac h (IdentityError error in result.Errors) { ModelState . AddMode l Error( "", error.Description); Большая часть кода в версии метода действия Edit () для запросов GET отвечает за генерацию наборов членов и не членов выбранной роли. После того как все поль­ зователи категоризированы, новый экземпляр класса RoleEdi tModel передается ме­ тоду View (), так что данные могут быть отображены с применением стандартного представления. Версия метода действия Edi t () для запросов POST отвечает за до­ бавление и удаление пользователей в и из ролей. Класс UserManager<T> предлагает методы для работы с ролями, которые описаны в табл. 29.7 . Таблица 29.7. Методы, связанные с ролями, которые определяет класс Userмanager<T> Имя AddToRoleAsync( user , narne) Ge tRolesAsync(user) IsinRoleAsync(user, narne) RemoveFrornRoleAsync(user ,name) Описание Добавляет идентификатор пользователя к роли с указанным именем Возвращает список имен ролей, членом которых является пользователь Возвращает true , если пользователь имеет членство в роли с указанным именем Удаляет пользователя как члена из роли с указанным именем Причудливость методов, относящихся к ролям, связана с тем, что они опериру­ ют с именами ролей , хотя роли имеют также и уникальные идентификаторы. По этой причине класс модели представления RoleModificationModel имеет свойс­ тво RoleName. В листинге 29.13 приведено содержимое файла Edi t . cshtrnl . добав­ ленного в папку Views/RoleAdrnin, который позволяет пользователю редактировать членство в роли.
Глава 29 . Применени е ASP.NET Core ldentity 917 Листинг 29.13. Содержимое файла Edit . cshtml из папки Views/RoleAdmin @model RoleEditModel <div class= " b g- primary panel - body " ><h4>Edit Ro l e</ h 4></di v> <div asp - val i dation- summary= " Al l" c la ss= " text - danger " ></div> <form asp - action="Edit " method= " pos t" > <input type= " hidden " name= " roleName " value= " @Model . Role . Name " /> <input type= " hidden " name =" roleid " val ue= " @Model.Role . Id " /> <hб class= " bg -i nfo pane l -body " >Add То @Model.Ro l e . Name</hб> <tа Ые class= " taЫe taЫe - bordered taЫe - condensed " > @if (Model . NonMembers . Count() = = 0) { <tr><td colspan= " 2 ">Al l Users Are Members</t d></tr> else { @foreach (AppUser user i n Mode l.N o n Members) <tr> <td>@user . Us erName</td> <td> <input type= " checkbox " name= " IdsToAdd " value =" @user.Id " > </td> </tr> </tаЫе> <hб class= " bg -i nfo panel - body " >Re mo v e From @Model . Role . Name</hб> <tаЫе class= " taЫe taЫe - bordered ta Ы e - condensed " > @if (Model . Members . Count() == 0) { <tr><td colspan= " 2 " >No Users Are Members</td></tr> else { @foreach (AppUser user i n Mode l. Me mbers) <tr> <td>@user . UserName</td> <td> <input type= " checkbox " nam e=" IdsTo Delete " value= " @u ser .I d " > </td> </tr> </tаЫе> <button type= " submit " class= " btn Ьt n - pr i mary " >Save</button> <а asp - action= "I ndex " class= " btn btn - default " >Cancel</a> </form> Представлени е содержит дв е таблицы: одну для пользов ателей, н е являющихся членами выбранной роли, и еще одну для пользовател ей, принадлежащих роли. Имя каждого пользователя отображается вместе с флажком, который позволяет изменять чл енство . Таблицы находятся внутри формы , которая отпр авляется методу действия Edit () и привязана к кл ассу мод ели RoleModificationModel, обеспечивая легкий доступ к списку изменений членства в роли .
918 Часть 11 . Подробные сведения об инфрастру ктуре ASP.NET Core MVC Тестирование редактирования членства в роли Чтобы протестировать поддержку членства в ролях, запустите приложение, пе­ рейдите на URL вида /RoleAdmin и создайте новую роль по имени Users. Щелкните на кнопке Edit (Редактировать) ; все пользователи в приложении отобразятся внутри списка не членов (рис. 29.6). l Users ~ ~ С t)i.;call,ost:б2542/Rol.;дd;;;-i.n/Edit/f2910ea6-:-;-734.4t;;:i=;;926.0"d1cз641if6f --~( ;: --~----~.-:.----·---~ - ~- - _____1 ВоЬ [;J Joe о Alice о No Users Are Members - Cancel 1 i...... ------- ------- ------ -----·- ------- ·------ --___ J Рис. 29.6 . Просмотр и редактирование членства в ролях Отметьте флажки для пользователей Alice и Joe (две из учетных записей, добав­ ленных в систему Identity в начале главы) и щелкните на кнопке Save (Сохранить) . В списке членов роли Users появятся пользователи Alice и Joe, как показано на рис. 29.7 . ! Uш1 ·+- -1 ' С' [J localhos t:625 42/Ro leAdrnir1 !----·---·-··--·-· . ----'-"-'---·"'.:.с......;,__.'-'-__ _. __ . . -·--· ---- 1ю 1 2ea3d582· 1f5b-4:kЗ-8602-b997e38df992 1291 oea6-1734 -4fa4-a926·0d1 c364 17f6f Name Users Admins No Users Use rs Joe. Alice ,. Рис. 29. 7 . Управление членством в ролях "" ."
Глава 29. Применение ASP. NЕТ Core ldentity 919 Использование ролей для авторизации Те перь, когда в приложении имеются роли , их можно применять в качестве осно­ вы для авторизации посредством атрибута Authorize . Чтобы облегчить тестирова­ ние автори зации на основ е ролей, добавьте в контроллер Account метод Logout () , который сделает возможным выход и последующий вход от имени другого пользова­ теля для де монстрации членства в ролях (листинг 29.14). Листинг 29.14. Добавление метода Logou t () в файле AccountCon trol ler. cs using System . Threading .T asks ; using Microsoft .As p NetCore . Author i zation ; using Microsof t. AspNetCore . Mvc ; using Users . Models ; us i ng Microsof t . Asp NetCore .I de ntity; namespace Use r s.Contro l lers ( [Authorize] puЫic class AccountController : Controller ( private UserManager<AppUser> use r Manager ; private SigninManager<AppUser> s igninManager ; 11 .. . для краткости другие методы действий не показаны .. . [Authorize] puЫic async Task<IActionResult> Logout() { await signinМanager.SignOutAsync(); return RedirectToAction ("Index", "Home"); Обновите контроллер Home , добавив новый метод действия и передав пр едставле­ нию инфор мацию об аутентифицированном пользовател е (листинг 29.15). Листинг 29.15. Добавление метода действия и информации об учетной записи в файле HomeController. cs us i ng System.Coll ections . Ge n eric ; using Microsof t. AspNetCo r e . Mv c ; using Microsoft . AspNetCore . Authorization ; namespace Users . Controllers { puЫic class HomeController : Contro l ler ( [Authorize] puЫic IActionResul t Index () => View (GetData (nameof (Index))) ; [Authorize(Roles = "Users")] puЬlic IActionResul t OtherAction () => View ( "Index" , GetData(nameof(OtherAction))); private Dictionary<string, object> GetData(string actionName) => new Dictionary<string, object> { [ "Action"] = actionName, }; [ "User"] = HttpContext. User. Identity . Name, [ "Authenticated"] = HttpContext. User. Identity. IsAuthenticated, ["Auth Туре"] = HttpContext.User.Identity . AuthenticationType, ["In Users Role"] = HttpContext.User.IsinRole("Users")
920 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Атрибут Aut horize для метода действия Index () не изменялся, но в случае его применения к методу OtherAction () было установлено свойство Roles с целью ука­ зания на то, что доступ к этому методу должен быть разрешен только членам роли Users. Кроме того, определен метод GetData (),который добавляет базовые сведения об удостоверении пользователя с использованием свойств, доступных через объект HttpContext. Совет. Атрибут Authorize можно также применять для авторизации доступа на основе списка индивидуальных пользовательских имен. Это привлекательная возможность в не­ больших проектах , но она требует изменения кода в контроллерах всякий раз , когда изме­ няется набор авторизуемых пользователей, и обычно означает необходимость в повтор­ ном проходе через цикл тестирования и развертывания. Использование для авторизации ролей изолирует приложение от изменений в отдельных пользовательских учетных запи­ сях и позволяет управлять доступом к приложению посредством информации о членстве в ролях, хранящейся в системе ASP.NET Core ldeпtity. Последнее изменение касается файла Index. cshtml из папки Views/Home, кото­ рый применяется обоими действиями в контроллере Home, и связано с добавлением ссылки, нацеленной на метод Logout () контроллера Account (листинг 29.16). Листинг 29.1 б. Добавление ссылки на метод Logout () в файле Index. cshtml из папки Views/Home @model Dictionary<string , object> <div class="bg-primary panel-body"><h4>User Details</h4></div> <tаЫе class= "taЫe taЫe-condensed taЬle-bordered"> @foreach (var kvp in Model) { <tr><th>@kvp.Key</ th> <td>@kvp.Value</td></t r> </tаЫе> @if (User?.Identity?.IsAuthenticated ?? false) { <а asp-controller="Account" asp-action="Logout" class="Ьtn ьtn-danger">Logout</a> Чтобы протестировать аутентификацию, запустите приложение и перейдите на URL вида /Ноте/ Index. Браузер будет перенаправлен так, что можно ввести поль­ зовательские учетные данные . Не имеет значения, данные какого пользователя из табл. 29.2 будут выбраны, поскольку атрибут Authorize, примененный к действию Index , разрешает доступ любому аутентифицированному пользователю . Однако если запросить URL вида /Home/OtherAction, то выбор пользователя из табл. 29.2 будет иметь значение, т.к. членами роли Users, требующейся для доступа I< методу OtherAction (), являются только пользователи Alice и Joe. В случае вхо­ да от имени пользователя ВоЬ браузер будет перенаправлен на URL вида /Account/ AccessDenied , который используется, когда пользователь не имеет возможности получить доступ к методу действия. Для обработки данной ситуации в контроллер Account добавлен метод AccessDenied (),так что теперь имеется действие, обраба­ тывающее запрос (листинг 29. 17) .
Глава 29 . Применение ASP.NET Соге ldentity 921 С овет. Устанавливая свойство I denti t yOption s . Cookies . Appl i ca tion Cooki e . AccessDen i edPat h , можно изменять URL вида / Accou nt/AccessDenied. Во врезке " Изменение URL для входа" ранее в главе был приведен похо ж ий пример . Л истинг 29. 17 . Доба вление метода дейст ви я в файле AccountController.cs using System . Threading . Tasks ; using Microsoft . AspNetCore . Authori zation; usi ng Microsoft.AspNetCore .Mvc; using Users.Mode l s ; using Mi crosoft.AspNetCore . Ide n t ity ; namespace Users . Control l ers { [Authorize] puЫic class AccountContro l ler : Contro l ler { private UserManager<AppUser> use r Manager ; private SigninManager<AppUser> s i gninManager ; puЫic AccountContro lle r (Use r Ma n age r <AppUser> u serMgr , Sign i nManager<AppUser> s i gninMg r ) { userManager = userMgr ; signinManager = signinMgr ; // .. . для краткости другие методы действий не показаны . .. [AllowAnonymous] puЫ i c IActionResult AccessDenied() returnVi.ew(); Чтобы сна бдить действие Acces s Den ied представление м для отображения, со­ здайте в папке Vi ews/Account файл по имени AccessDe n i ed . csh t ml с содержимым из л ис тинг а 29.18. Листи нг 29.18. Содер жи мое файла AccessDenied. cshtml из папки Views/Account <div class= " bg - danger pane l-body " ><h4>Access Denied</h4></div> <а asp - action=" Index " a sp-controlle r="Home " class= " btn Ьt n-prima ry " >OK</a> Запустите приложение, запросите URL вида /Account/Log i n и войдите от име­ ни bob@examp l e . com . Когда процесс аутентификации завершится , бр аузер будет п е ­ ренаправлен на URL вида / Home/ I ndex , который отобразит детали уче тно й з аписи, как показано слева на рис. 29.8, проясняя, что пользователь ВоЬ не является чле­ ном роли Use r s . Затем запросите URL вида / Home/Oth erAction, который нацел ен на действ ие, защищенное с помощью доступа на основе ролей. Пользователь ВоЬ не имеет требуемого членства в роли , поэтому браузер будет перенаправлен на URL вида /Account/Ac c essDenied, как демонстрируется справа н а рис. 29.8 . Совет. Роли загружаются во время входа пользователя, т.е. изменение ролей для пользова­ теля, который в текущий м омент аутентифицирован, вступит в силу только после того, ка к пользователь выйдет и затем аутентифицируется заново.
922 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC :~;;:-.··~;~"' ', 1 . . . , / >{,;;- , : ',r_•:.::1,:-,~Y.~' ;,U~er Detail~ ::. ·.. ):: ' 1 1~:.'. , ' • ' 1 • ' Action User Auth e ntlcat.ed Auth Тур е ln Users Rol e lndex ВоЬ Tru e Microsoft.AspNet.ldentity.Application False -----------------~ Рис. 29.8 . Применение авторизации на основе ролей Помещение в базу данных начальных данных В рассматриваемом проекте осталась одна проблема - доступ к контроллерам Admin и RoleAdmin не ограничен. Это классическая проблема курицы и яйца. Дело в том, что для ограничения доступа необходимо создать пользователей и роли, но кон­ троллеры Admin и RoleAdmin являются инструментами управления пользователями, и если защитить их с помощью атрибута Authorize, тогда не будет никаких учет­ ных данных, которые предоставят к ним доступ, особенно при первом развертывании приложения. Проблема решается помещением в базу данных начальных данных, когда приложе­ ние запускается. В листинге 29.19 приведено содержимое файла appsettings. j son с добавленными конфигурационными данными, указывающими детали для учетной записи, которая будет создана. Листинг 29.19. Добавление конфигурационных данных в файле appsettings. j son " Data": { "AclminUser": "Name 11 : "Admin", "Email": "aclmin@example.com", "Password": "secret", "Role": "Aclmins" }' " SportStoreidentity ": "ConnectionString": "Server= ( localdb) \ \MSSQLLocalDB ; Datab a se= IdentityUsers;Trusted Connection=True;Mu l tipleActiveResul t Sets=true" ) - В категории Data: AdminUser предоставлены четыре значения, требующиеся для создания учетной записи и ее назначения роли, которая обеспечит возможность ис­ пользования административных инструментов.
Глава 29. Применение ASP.NET Core ldentity 923 В нимание! Помещение паролей в текстовые конфигурационные файлы означает необхо ­ димость добавления в процесс развертывания приложения возможности изменять па­ роль стандартной учетной записи и инициализировать новую базу данны х при начальном развертывании. Добавьте в класс Appidenti tyDbContext статический метод, как показано в лис­ тинге 29.20. Код для создания стандартной учетной записи вовсе не обязан находить­ ся в данном классе, но для меня это место выглядит вполне естественным, и я посту­ паю так в своих проектах. Листинг 29.20. Добавление метода в файле AppidentityDbContext. cs using Microsoft . AspNetCore .Identity .E ntityFrameworkCore ; using Microsoft.EntityFrameworkCore ; using System.Threading.Tasks; using Microsoft . Extensions.Configuration; using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Dependencyinjection; namespace Users . Models { puЬlic class AppidentityDbContext : IdentityDbContext<AppUser> puЫic AppidentityDbContext(DbContextOptions<AppidentityDbContext> options) : base ( options) { } puЬlic static async Task CreateAdminAccount(IServiceProvider serviceProvider, IConfiguration configuration) { Userмanager<AppUser> userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>(); Roleмanager<IdentityRole> roleмanager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>(); string username = configuration["Data:AdminUser:Name"]; string email = configuration["Data:AdminUser:Email"]; string password = configuration [ "Data : AdminUser: Password"] ; string role = configuration [ "Data :AdminUser: Role"] ; if (await userManager.Find.ВyNameAsync(username) == null) { if (await roleМanager.FindВyNameAsync(role) == null) { await roleмanager.CreateAsync(new IdentityRole(role)); AppUser user = new AppUser UserName = username, Email = email }; IdentityResult result = await userManager .CreateAsync(user, password); if (result.Succeeded) { await userManager.AddToRoleAsync(user, role);
924 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Метод Cr eat eAdmi nAcc ount () принимает объект реализации I Ser vi ceProvi der, который прим е няется для получе ния объектов Use r Manager и RoleManager, а такж е объект реализации IConfigurati on, используем ый для извл еч ения данных из файл а app s etting . j son . Код в методе Crea teAdrninAccoun t () проверяет, существует ли пользовател ь , и если нет, то создает его и назнач ает указанной роли, которая таюк е при необходимости создается. В листинг е 29.21 в 1шасс Startup добав ле н опер атор. вызывающий метод CreateAdrni nAccount () после того , как остальная часть прило ­ жения настроена и сконфигурирована. Листинг 29.21. Вызов метода базы данных в файле Startup. cs p u Ыic void Configure(IApplicationBuilder арр) { app . UseStatus CodePages() ; app.UseDeveloperExceptionPage() ; app . UseStat i cFi les() ; app . Us eidentity() ; app . UseMvcWithDefaultRoute() ; AppidentityDbContext . CreateAdminAccount(app . ApplicationServices, Configuration) .Wait(); Имея надежную стандартную учетную запись в базе данных ldentJty, м ожно при­ менить атрибут Authorize для защиты контроллеров Adrnin и RoleAdrnin . В листин­ ге 29.22 пр и в едены изменения , внес енные в контроллер Adrnin . Листинг 29.22 . Ограничение доступа в файле AdminController. cs using Microsoft . AspNetCore . Ident i ty ; using Microsoft . AspNetCo re. Mvc ; u sing Users . Models ; us ing System . Th r eading .Tasks ; using Microsoft.AspNetCore.Authorization; namespace Users . Controllers { [Authorize (Roles = "Admins") ] puЫic class Adm i nControl l er : Contro ll e r { 11 ... для краткости опера торы не показаны . . . В листинге 29.23 показано соответствующее изменение , внесенное в контро лле р Rol eAdrni n. Листинг 29.23. Ограничение доступа в файле RoleAdminController. cs using System.ComponentModel . Da t aAnnotations ; using System . Threading . Tas k s ; using Microsoft . Asp Ne t Core. Ident ity ; u s ing Microsoft . AspNetCo r e . Identity . EntityFrameworkCore ; using Microsoft . AspNetCo r e . Mvc ; us i ng System.Linq ; using Users . Models ; using System. Co l lection s . Generic ; using Microsoft.AspNetCore.Authorization;
namespace Users . Contro lle rs { [Authorize (Roles = " Admins")] Глава 29 . Применение ASP. NET Core ldentity 925 puЫic class RoleAdminCo nt r oller : Contr oller // ...для краткости операторы не показаны . . . Запустите приложение и запросите URL вида /Admin или /RoleAdmi n. Если вы уже вошли от имени какого-то другого пользователя. тогда понадобится выйти. В про­ тивном случае вам будет предложено предоставить учетные данные . так что можете вв е сти admin@e x am pl e . с от и пароль secret и получить доступ к административ­ ным функциям. Резюме В настоящей главе объяснялось. как использовать систему ASP.NET Core Identity для аутентификации и авторизации пользователей . Вы узнали, каким образом соби­ рать и проверять пользовательские учетные данные и ограничивать доступ к методам действий на основе ролей. членом которых является пользователь. В следующей главе будут проде м онстрированы некоторые расширенные средства, предлагаемые систе­ мой ASP.NET Core Identity.
ГЛАВА 30 Расширенные средства ASP.NEТ Core Identity в этой главе описание системы ASP.NET Core ldentity завершается рассмотрением ряда расширенных средств, которые она предлагает. Будет продемонстрирова­ но, как расширять схему базы данных за счет определения специальных свойств в классе пользователя, и каким образом использовать миграции базы данных для при­ менения этих свойств, не удаляя данные из базы данных ASP.NET Core Identity. Кроме того, вы узнаете, как система ASP.NET Core Identity поддерживает концепцию заявок (claim), и ознакомитесь с их использованием для гибкой авторизации доступа к мето­ дам действий через политики. Ближе к концу главы будет показано, каким образом система ASP.NET Core Identity облегчает аутентификацию пользователей с участием третьих сторон. Вы увидите способ аутентификации с помощью учетных запис е й Google, но имейте в виду, что в ASP.NEТ Core Identity имеется встроенная поддержка также для учетных записей Microsoft, Facebook и Twitter. В табл. 30.1 приведена свод­ ка для настоящей главы. Таблица 30.1 . Сводка по главе Задача Сохранение специальных данных для пользователей Выполнение детализированной авторизации Создание специальных утверждений Использование данных утверждений для оценки доступа пользователей Применение политик для доступа к ресурсам Разрешение третьим сторонам выпол­ нять аутентификацию Решение Листинг Добавьте свойства в класс паль- 30.1-30.3 зователя и обновите базу данных ldeпtity Используйте утверждения 30.4-30.6 Применяйте трансформацию 30.7, 30.8 утверждений Создайте политики Оценивайте политики внутри ме­ тодов действий Принимайте утверждения от пос­ тавщиков аутентификации , таких как Microsoft, Google и Facebook 30 .9 -30 .13 30.14-30 .19 30.20-30 .23
Глава 30. Р асшире нн ые средства ASP.NEТ Core lden tity 927 Подготовка проекта для примера В главе будет продолжена работа с проектом Users, созданным в главе 28 и усо­ вершенствованным в главе 29. Запустите приложение и удостоверьтесь в наличии пользователей в базе данных. На рис . 30.1 показано состояние базы данных, 1юто ­ рая содержит пользователей Admin, Alice, ВоЬ и Joe из предыдущей главы . Чтобы проверить пользователей, запустите приложение, запросите URL вида /Admin и ау ­ тентифицируйтесь как пользователь Admin , используя адрес admin@ e xamp le .com и пароль secret. 1~. User~ х +- ~ С D localhost:6254 2/Ad rп in ------..:...·---:..:.--·---=-- - : --"'~--------------·..;, ._ ___ . - ' •• ... / •• • ' ~'' • :"~~ '- , '•' о",'~ '1 • • • ' i :, " User. Accounts - . · .., ":· .-, - "· - · " ' ~·"' ' • • • \.- • •.·:• • _' : 9 : ' • ' • • l 1 1О Na me Email 2fe2bВf9-e1 d9-4da4-a833-c4881ed01358 Admin admin@example .com "" 65 1aзbda-7fe5·45Зc-b32d- f50f1 a5126d2 ВоЬ bob@example .com "" 9787а 1Ьа-6726-4762-ас7d-57еЗеЗе9dс9с Alice alice@example.com ." bd71а9е7-d376 -4a2c-abbc -71 b87afcffe2 Joe joe@example.com "" *1 Рис. 30.1. Начальные пользователи в базе данных ldentity В настоящей главе также понадобится несколько ролей. Перейдите на URL вида /RoleAdmin , создайте роли с именами Use rs и Employees и назначьте им пользова­ телей согласно табл. 30.2 . Таблица 30 .2 . Роли и их члены , требующиеся для примера приложения Роль Члены Users Alice, Joe Employees Alice,Bob На рис . 30. 2 показана требуемая конфигурация ролей, отображаемая !{ОНтролле­ ром RoleAdmin . Добавление специальных свойств в класс пользователя При создании класса AppUser для представления пользователей в главе 28 было упомянуто , что базовый класс определяет основной набор свойств, описывающих пользователя, таких как адрес электронной почты и телефонный номер.
928 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC User~ х г,:;---·----·----- - --- ·-·---·-------·----------- +- 'i'> С [] localhos t:62542/RoleAclmin/Edit/e7861409- 3b9e- 4f81-8aOb-c50c 146abef7 f..? _ ·--~---------· ID Name Users c56fd8dd-3336 -4340-a775-e4e57959d235 Admins Adm in d71e56ad - 84ac - 4 3cf-b60З-3643Ьf4cf6c1 Use rs Allce , Joe e786 1 409-Зb9e -4f81-8a0b-<:50c146abe f7 Employees ВоЬ, Alice Рис. 30.2. Конфигурирование ролей для целей главы В большинстве приложений необходимо хранить дополнительную информацию о пользователях , включая постоянные предпочтения в приложении и сведения вроде адресов - короче говоря, любые данные, которые полезны при выполнении прило­ жения и должны предохраняться между сеансами. Поскольку по умолчанию для хра­ нения своих данных система ASP.NET Core Id entity применяет инфраструктуру Entity Framework Core, определение дополнительной информации о пользователе означает добавление свойств в класс пользователя и предоставл ение EF Core возможности со­ здать схему базы данных для их сохранения. В листинге 30.1 приведен код класса AppUser с добавленными двумя простыми свойствами, предназначенными для представления города, в котором живет пользо­ ватель , и уровня его квалификации. Листинг 30.1. Добавление свойств в файле AppUser. cs using Microsoft . AspNetCore . Identity .EntityFrameworkCore; namespace Use rs.Models { puЬlic enum Ci ties { None, London, Paris, Chicago puЫic enum QualificationLevels None, Basic, Advanced pu Ыic class AppUser : IdentityUser puЬlic Ci ties Ci ty { get; set; } puЫic QualificationLevels Qualifications { get; set; } Перечисления Ci tie s и QualificationLevels определяют значения для не­ скольких городов и уровней квалификации. Эти перечисления используются свойс­ твами City и Qualification, добавленными в класс AppUs er.
Глава 30. Расширенные средства ASP.NET Core ldentity 929 Действия, которые добавлены к контроллеру Home в листинге 30.2, позволяют поль­ зователю просматривать и редактировать свои свойства City и Qualification. Листинг 30.2 . Добавление поддержки для специальных свойств из класса пользователя в файле HomeController. cs using Systern . Collections . Generic ; using Microsoft . AspNetCore . Mvc ; using Microsoft . AspNetCore . Authorization; using Users.Models; using Мicrosoft.AspNetCore.Identity; using System.Threading.Tasks; using System.Componentмodel.DataAnnotations; namespace Users . Controllers { puЫic class HomeControl le r : Controller { private UserManager<AppUser> userмanager; puЫic HomeController(Userмanager<AppUser> userMgr) { userManager = userMgr; [Authorize] puЫic IActionRe sult Index() => View(GetData(narneof(Index))) ; [Authorize(Roles = "U sers " ) ] puЫic IActionResul t OtherAction () => View ( " Index ", GetData(narneof(OtherActi on))) ; private Dictionary<string , object> GetData(string actionName) => new Dictionary<string , object> { ["Action"J = actionName , [ "User "J = HttpContext . User.Identity.Name, [ "Authenticate d "] = HttpContext . User . Identity.IsAuthenticated, [ "Auth Туре " ] = HttpContext.User .Identity . Authen t icationType , ["In Users Role "] = HttpContext . User . IsinRo l e( "U sers " ) , ["City"] = CurrentUser.Result.City, ["Qualification"] = CurrentUser.Result.Qualifications }; [Authorize] puЫic async Task<IActionResult> UserProps() return View(await CurrentUser); [Authorize] [HttpPost] puЫic async Task<IActionResult> UserProps( [Required]Cities city, [Required]QualificationLevels qualifications) if (ModelState. IsValid) { AppUser user = await CurrentUser; user.City = city; user.Qualifications = qualifications; await userManager.UpdateAsync(user); return RedirectToAction("Index"); return View(await CurrentUser); private Task<AppUser> CurrentUser => userManager.FindByNameAsync(HttpContext.User.Identity.Name);
930 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Ново е свойство Cu r rentUser с помощью класса UserManager<AppUser> извле­ кает экземпляр App User для представления текуlli;его пользователя. Объект AppUser применя ется в качестве объекта модели представления в версии GET метода действия UserProps (), а версия POST этого метода использует его для обновления з на ч ений новых св ойств City и QualificationLeve l. Метод Ge tData ( ) был модифицирован так, чтобы возвращаемый им словарь со­ держал значения специальных свойств для текущего пользователя, т.е. значения спе­ циальных свойств будут видны в представлениях, которые отображаются методами действий Index () и OtherAction (). Чтобы снабдить методы действий Us e r Props () представлением , добавьте в пап­ ку Views/ Home файл по имени UserP r ops. cshtml и поместите в него разметку из листинга 30.3 . Листинг 30.З. Содержимое файла UserProps. cshtml из папки Views/Home @model AppUse r <div class= " bg - primary pa n el - body " ><h4>@Mode l. UserName</h4></div> <div asp- validati on- summary= "Al l" class= " text-danger " ></di v> <form asp- actio n="U serProps " me t hod="post" > <div cl ass=" form- group"> <label asp- for=" City"></l abel> <select asp- for= " City" class=" for m- contr ol" a sp- items= " @new Sel e ctList (Enum . GetName s(typeof(Cities) ) ) " > <option di sa Ыed selected v al ue="" >Select а Ci t y</option> </se l ect> </di v> <div class="form- group " > <label asp -for= " Qualifications " ></ l abe l > <select asp - for= " Qualifications" class= " form - control " asp - items= " @n ew SelectList(Enum . Ge t Names(typeof(QualificationLevels) )) " > <option d i saЫed selected val u e ="" >Se l ect а City</option> </select> </div> <butt o n typ e=" submit " class= " Ьtn Ьtn -p r i mary " >Submit</button> <а asp- acti on=" Index" class= "Ьtn Ьtn-default" >Cancel</a> </ f orm> Представление содержит форму с элементами s elect, которые заполняются зна­ чениями из перечислений, опр еделенных в листинге 30.1. Когда форма отправляет­ ся, из базы данных Identity извлекается объект AppUs er , представляющий те ку щего пользователя, а значения специальных свойств обновляются с применением значе­ ний , выбранных пользов ателем : AppUser us er = awai t CurrentUser; user.City = city; user.Qualifications = qualifications; await userManager.UpdateAsync(user); return Redir e ctToAct i on( " Index " ) ;
Глава 30. Расширенные средства ASP.NET Core ldentity 931 Обратите внимание , что диспетчеру пользователей потребу ется явно указать на необходимость обновления записи базы данных для пользователя , чтобы отразить из­ менения. путем вызова метода Up dateA s y n c ( ). Ранее делать это было не обязатель­ но, т.к. внутри м етодов, которые использовались для внесения изменений в Id entity, метод UpdateAsyn c () вызывался автоматически, но при изменении свойств напря­ мую ответственность за сообщение диспетчеру пользователей о том, что нужно вы­ полнить обновление, возлагается на вас. Подготовка миграции базы данных Все связующие механизмы приложения, предназначенные для поддержки новых свойств, теперь на месте, и осталось лишь обновить базу данных, чтобы ее таблицы сохраняли значения специальных свойств. Первым делом понадобится создать новый файл миграции базы данных, кото­ рый будет содержать команды SQL, требуемые для обновления схемы базы данных. Откройте окно консоли диспетчера пакетов и введите следующую команду: Add - Migrat i on Cus tomProp erti e s После завершения команды вы заметите в палке Mi gra t ion s новый файл , в име ­ ни которого содержится строка Cus tomP rop ert ies , а точное имя включает числовой идентифюштор. Открыв этот файл , вы увидите в нем класс С#, содержащий метод по имени Up (), который выполняет команды SQL, необходимые для добавления в базу данных поддержки специальных свойств. Имеется также метод Dow n ( ) , который вы­ полняет команды. восстанавливающие предыдущую схему базы данных. Внимание! Инструменты Entity Framework Core текущей версии не добавляют в файл миграций про­ странство имен, которое содержит классы моделей. Для устранения проблемы придется отре­ дактировать в папке Mi grati o n s файл, имя которого содержит строку CustomPropert ies , и добавить в его начало оператор using для пространства имен Us e rs. Mod el s . Затем с помощью показанной ниже команды выполняется миграция базы данных в новую схему: Updat e- Database Когда команда завершится, таблица базы данных , которая хранит данные о поль­ зователях , будет содержать новые столбцы, представляющие специальные свойства. Внимание! Будьте внимательны при выполнении миграций производственных баз данных, содержащих реальные данные о пользователях. Помните, что довольно легко создать миграцию , которая отбросит столбцы или целые таблицы и может привести к разруши­ тельным последствиям. Удостоверьтесь в том, что миграции баз данных тщательно про­ тестированы, и позаботьтесь о создании резервной копии важных данных на случай , если что-то пойдет не так, как ожидалось. Тестирование специальных свойств Чтобы протестировать результат проведенной миграции , запустите приложение и войдите как один из пользователей, хранящихся в Identity (с применением , например , адреса alice@exampl e . сот и пароля secre t123). После аутентификации вы увидите стандартные значения для свойств Ci ty и Qualificati o n Lev e l. Изменить эти свойс-
932 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC тва можно, запросив URL вида /Horne/UserProps , выбрав новые знач ения и щелкнув на кнопке Submit (Отправить), что приведет к обновлению базы данных и перенаправ ­ лению опять на URL вида / Ноте с отображением новых значений (рис. 30.3) . Acll on lndex Use r Aullieпlicated Auth Туре ln Users Role I J City Oualifi cati on l." Tru e None None London Oualiflcatlons Action Use r Autheпticated Auth Туре 1 Advanced ln Users Role • Cance l L' j Ou alilical ion - -------l - --L__ lndex Alice Tru e Mlcrosofl .AspNet. lde ntity.Application Tr ue London Advanced Рис. 30.З. Использование специальных свойств в классе пользователя Работа с заявками и политиками В более старых системах управления пользователям и, таких как ASP.NET Membership , которая предшествовала ASP.NET Core Identity, приложение считалось полномочным источником всей информации о пользователе , по существу трактуя приложение как замкнутый мир и доверяя содержащимся в нем данным. Такой подход к разработке ПО укоренился настолько прочно, что может быть труд­ но осознать, что это произошло. Пример приема с замкнутым миром приводился в главе 29, когда пользователи аутентифицировались с пом ощью уч етных данных, хра­ нящихся в базе данных, а доступ предоставлялся на основе ролей, ассоциированных с учетными данными. То же самое делалось еще раз в настоящей главе, когда в класс пользователя были добавлены специальные свойства. Каждый фрагмент информа ­ ции, необходимой для управления аутентификацией и авторизацией пользователей , поступал изнутри приложения; такой подход полностью подходит для большинства веб - приложений , и потому он демонстрировался здесь настолько подробно. Система ASP.NEТ Core ldentity также поддерживает альтернативный подход к рабо­ те с пользователями, который приемлем в ситуации, когда приложение МVС не является единственным источником информации о пользователях, и может применяться для ав­ торизации пользователей более гибкими способами, чем позволяют традиционные роли. Альтернативный подход предусматривает использование заявок, и в этом разделе будет описано , как система ASP.NEТ Core Id entity поддерживает авторизацwо на основе заявок. Совет. Вы вовсе не обязаны применять заявки в своих приложениях и, как было показано в главе 29 , система ASP.NET Core ldeпtity прекрасно обеспечивает приложения службами аутентификации и авторизации безо всякой потребности в понимании заявок.
Глава 30. Расширенные средства ASP.NET Core ldentity 933 Понятие заявок Заявка - это порция информации о пользователе наряду со сведениями о том, откуда информация поступила. Изучать заявки легче всего на практических демонс­ трациях, в отсутствие которых любое обсуждение становится слишком абстрактным, чтобы быть по -настоящему полезным. Прежде всего, добавьте в папку Co ntro llers ф айл класса по имени Claims Con t ro ll er . cs и определите в нем контроллер, как пока зано в листинге 30.4 . Совет. Вы можете пребывать в некоторой растерянности, следя за определением кода и зна­ комясь с описаниями классов для текущего примера. В настоящий момент переживать по поводу деталей не стоит - просто придерживайтесь инструкций до тех пор, пока не уви­ дите вывод из метода действия и представление, которое будет определено. Это лучше всего другого содействует пониманию заявок. Листинг 30.4 . Содержимое файла ClaimsController. cs из папки Controllers using Mi crosoft . AspNetCore. Aut h o r iza ti on ; us i ng Mi crosoft . AspNetCore . Mvc ; namespace Users . Co n troller s { puЫic cl ass ClaimsControll er : Contr oller { [Authorize] puЫic ViewResult Index() => View( Use r?. Cla i ms); Получать заявки, ассоциированные с пользователем, можно разными путями. Свойство User (также доступное как свойство HttpContext . User) возвращает объ­ ект ClaimsPrincipal , который реализует принцип, задействованный в данном при­ мере . Набор заявок, связанных с пользователем, доступен через методы и свойства класс а ClairnsPrinci pal , перечисленные в табл. 30.3 . Таблица 30.З. Изб ранные члены класса ClaimsPrincipal Имя Identity FindAll (type) FindAll(<predicate>) FindFirst(type) FindFirst(<predicate>) HasClaim(type , value) HasClaim(<predicate>) IsinRo l e(name) Описание Получает объект реализации Пdentity , который ассо­ циирован с текущим пользователем, как рассказывается в последующи х разделах Эти методы возвращают все заявки специфического типа или те, которые удовлетворяют предикату Эти методы возвращают первую заявку специфического типа или ту, которая удовлетворяет предикату Эти методы возвращают true , если пользователь имеет заявку указанного типа с заданным значением или если есть заяв ка, удовлетворяющая предикату Возвращает t r ue , если пользователь является членом роли с указанным именем
934 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Как объяснялось в главе 28, свойство HttpContext. User. Identi ty возвра­ щает реализацию интерфейса IIdenti ty, которой в случае использования ASP.NET Core Identity является объект Claimsidentity. В табл. 30.4 описаны члены класса Claimsidentity, имеющие отношение к этой главе. Таблица 30.4. Избранные члены класса Claimsidentity Имя Claims Описание Возвращает перечисление объектов Claim, представляющих заявки для пользователя AddClaim(claim) AddClaims(claims) Добавляет заявку к удостоверению пользователя Добавляет перечисление объектов Claim к удостоверению пользователя HasClaim(predicate) Возвращает true, если удостоверение пользователя содер­ жит заявку, которая совпадает с указанной RemoveClaim(claim) Удаляет заявку из удостоверения пользователя Доступны также другие методы и свойства, но перечисленные в табл. 30.4 чле­ ны применяются в веб-приложениях наиболее часто по причинам, которые станут очевидными по мере демонстрации места заявок в рамках более широкой платфор­ мы ASP.NEТ Core. В листинге 30.4 используется свойство Controller. User для получения объекта ClaimsPrincipal и передачи значения свойства Claims как модели представления стандартному представлению. Объект Claim представляет одиночную порцию дан­ ных о пользователе, а в классе Claim определены свойства, показанные в табл. 30.5 . Таблица 30.5. Свойства класса Claim Имя Issuer Subject Туре Value Описание Возвращает имя системы, которая предоставила заявку Возвращает объект Claimsidenti ty для пользователя, к которому относится заявка Возвращает тип информации, которую представляет заявка Возвращает порцию информации, которую представляет заявка Чтобы отобразить детали заявок, ассоциированных с пользователем, создайте папку Views/Claims и добавьте в нее файл по имени Index. cshtml с содержимым из листинга 30.5 . Листинг 30.5. Содержимое файла Index. cshtml из папки Views/Claims @model IEnumeraЫe<System.Security.Clairns.Clairn> <div class="bg-primary panel-body"><h4>Clairns</h4></div> <tаЫе class="taЫe taЫe-condensed taЬle-bordered"> <tr> <th>Subject</th><th>Issuer</th><th>Type</th><th>Value</th> </tr>
Глава 30. Расширенные средства ASP.NET Core ldentity 935 @if (Model == null 11 Model.Count () == О) { <tr><td colspan= " 4 " class= " text - center " >No Claims</td></tr> else { @foreach (var claim in Model.OrderBy(x => х . Туре)) { <tr> <td>@claim . Subject . Name</td> <td>@claim.Issuer</td> <td identity- claim-type="@claim.Type " ></td> <td>@claim.Value</td> </tr> </tаЫе> Для отображения заявок, переданных в модели представления, применяется таб­ лица. Значением свойства Claim. Туре является URI для схемы Microsoft, которая не особенно полезна. В качестве значений для полей класса System. Securi ty . Claims . ClairnTypes используются популярные схемы, поэтому чтобы облегчить чтение выво­ да из представления Index . cshtml, к элементу td, отображающему свойство Туре, был добавлен специальный атрибут: <td identity-claim-type="@claim.Type"></td> Добавьте в папку Infrastructure файл класса по имени ClairnTypeTagHelper . cs и создайте в нем дескрипторный вспомогательный класс, который транслирует зна­ чение атрибута в более читабельную строку (листинг 30.6). Листинг 30.6 . Содержимое файла ClaimТypeTagHelper. cs из папки Infrastructure using System .Linq; using Systern . Reflection ; using Systern . Security.C lairns; using Microsoft .AspNetCore . Razor .TagHelpers ; namespace Users . Infras tructure { [HtrnlTargetElernent( " td ", Attributes = " identity- clairn- type " )] puЫic class ClaimTypeTagHelper : TagHelper { [HtrnlAttributeNarne( " identity-claim- type " )] puЬlic string ClaimType { get; set ; } puЬlic override void Process(TagHelperContext context, TagHelperOutput output) { bool foundType = false; Fieldinfo[] fields = typeof(ClairnTypes) .GetFields() ; foreach (Fieldinfo field in fields) { if (field . GetValue(null) .T oString() == ClaimType) output . Content . SetContent(field . Narne); foundType = true ; if (!foundType) { output . Content . SetContent(ClairnType . Split( ' /' , '. ' ) .Last());
936 Часть 11. П одробные сведения об инфраструктуре ASP.NET Core MVC Чтобы выяснить, поч ему контроллер, в котором применяются заявки , был создан без подробных объяснений, запустите приложение и аутентифицируйтесь как поль­ зователь Alice (используя адрес a lice@exarn ple . corn и пароль secret123). После прохождения аутентификации запросите URL вида /C l aims для просмотра заявок, ассоциированных с пользователем (рис. 30.4). Sub ject lss uer Туре Val ue Alice LOCAL AUTHORITY Secu rityStamp ebЬd9127-5bbe-49f2-BOce-OOOЗ6076690d Alice LOCAL AUTHORIТY Rol e Use rs Alice LOCAL AUTHORITY Role Employees Alice LOCAL AUTHORIТY Name Alice Alice LO CAL AUTHOR I ТY Name/dentifier 97В7а 1Ьа-6726 -4762 -a c7d-57 e3e3e9dc9c Рис . 30.4 . Вывод из действия I ndex контроллера Clairns Для удобства детали заявок дополнительно воспроизведены в табл . 30.6. Таблица 30 .6. Данные, показанные на рис. 30.4 Субъект Выдавшая система Тип Значение Al ice LOCAL AUTHOR I TY Secu r ityStarnp Уникальный идентификатор Alice LOCAL AU THOR I TY Role Ernp loyees Alice LOCAL AUT HORITY Ro l e Users Alice LOCA L AUT HOR ITY Narne Alice Alice L OCAL AUTHORIT Y Name!dentifier Идентификатор пользователя Alice В табл. 30.6 отражен самый важный аспект заявок , который зюиrючается в том, что они уже применялись при реализации стандартных ср едств аутентификации и авторизации в главе 29. Здесь видно, что некоторые из заявок относятся к удостове ­ рению пользователя (заявка Name - это A l i ce , а заявка Name!dent i f ie r -уникаль­ ный идентификатор пользователя Alice в базе данных ASP.NET Core Identity). Друrие заявки показывают членство в ролях - есть две заявки Ro l e , свид етельствующие о том факте, что пользователю Alice назначены роли User s и Emp l oyees. Когда информация подобного рода выражается как набор заявок , отличие состоит в том , что появляется возможность определить, откуда поступили данные. Для всех заявок, приведенных в табл . 30.6 , свойство I ss u e r установл ено в L OCAL AUTHORITY , указывая на то, что удостоверение пользователя бьmо установлено приложением .
Глава 30. Расширенные средства ASP.NET Core ldentity 937 Теперь, когда вы увидели примеры заявок, определить, что собой представляет за ­ явка, гораздо легче: заявка - это любая порция информации о пользователе, которая доступна приложению, включая удостоверение пользователя и его членство в ролях. Кроме того. информация о пользоват елях, которая была определена в пр едшествую­ щих главах, автоматически делается доступной в виде заявок системой ASP.NET Core Identity. Хотя поначалу заявки могут сбивать с толку, ничего магического в них нет, и подобно любому другому аспекту приложений МVС, они оказываются гораздо менее пугающими, как только заглянуть "за кулисы" и выяснить, каким образом они на са­ мом деле работают. Создание заявок Заявки интересны тем, что приложение может получать их из многих источников, а не просто полагаться на локальную базу данных, хранящую информацию о пользовате­ лях. В разделе "Использование сторонней аутентификации" далее в главе на реальном примере будет показано, как аутентифицировать пользователей с помощью сторонней системы, а пока в проект нужно добавить класс, эмулирующий систему, которая пре­ доставляет информацию о заявках. Добавьте в папку Infrastructure файл класса по имени LocationClaimsProvider. cs с содержимым из листинга 30.7 . Листинг 30. 7. Содержимое файла LocationClaimsProvider. cs из папки Infrastructure using System . Security . Claims ; using System . Threading.Tasks ; using Microsoft .AspNetCore .Authentication ; namespace Users. I nfrastructure { puЫic static class LocationClaimsProvider puЫic static Task<ClaimsPrincipal> AddClaims( ClaimsTransformationContext context) { ClaimsPrincipal principal = context . Principa l; if (principal != null } && !principal.HasClaim(c =>с.Туре== ClaimTypes . PostalCode)) Claimsidentity identity = principal.Identity as Cl a imsidentity ; if (identity != null && identity . IsAuthenticated && identity . Name != null) { if (identity . Name . ToLower() == "alice") ( identity.AddClaims(new Claim[] { CreateClaim(Cl aimTypes .PostalCode , "D C 20500 " ) , CreateClaim(ClaimTypes . StateOrProvince , " ОС " ) )); else { identity . AddClaims(new Claim[ ] { CreateClaim(ClaimTypes . PostalCode , " NY 10036 " ) , CreateClaim(ClaimTypes .StateOrProvince , " NY") )); return Task.FromResult(principal); private static Claim CreateClaim(string type, string value) => new Claim(type , value , ClaimValueTypes.String , " RemoteCla i ms " );
938 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Метод AddClaims () принимает объект Cla i msTransformationContext и полу­ ча ет ассоциированный с ним экземпляр ClaimsPrincipal. чтобы прив ести значе­ ние его свойства Identity к типу Claimsidenti ty. Затем значение свойства Name применяется для создания заявок, касающихся почтового кода и штата, в котором проживает пользователь. Класс LocationClaimsProvider эмулирует систему вроде центральной базы дан­ ных человеческих ресурсов, которая могла бы служить полномочным источником ин­ формации о месте ж ительств а персонала. н апример . Для применения специальных заявок необходимо включить функцию промежу­ точного ПО, известную как трансформация заявок, которая будет вы зывать метод AddClaims () класса LocationClaimsProvider при получении каждого запроса ин­ фраструктурой ASP.NET Core. В листинге 30.8 функция трансформации заявок вклю­ чается в методе Configure () класса Startup. Листинг 30.8. Включение трансформации заявок в файле Startup. cs puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app . UseStaticFiles() ; app . Use!dentity() ; app.UseClaimsTransformation(LocationClaimsProvider.AddClaims); app . UseMvcWithDefaultRoute() ; AppidentityDbContext . CreateAdminAccount(app . ApplicationServices , Configuration) . Wait() ; Метод UseClaimsTransformation () используется для назначения метода. кото­ рый будет получать объект ClaimsPrincipal и трансформировать его; здесь указан статический метод AddClaims () класса LocationClaimsProvider. Трансформация заявок Применение средства трансформации заявок требует определенной осторожности, пос­ кольку указываемый метод вызывается для инспектирования - и возможной модифика­ ции - объекта ClaimsPrincipal, ассоциированного с каждым запросом, что означает необходимость избегать выполнения затратных или медленных операций. При наличии многочисленных трансформаций, подле жащих выполнению, которые обычно необходимы для интеграции данных о заявках из различных источников, можно использо­ вать удобный класс по имени ClaimsTransformer : Cl aimsTransformer transform = new ClaimsTransformer(); transform . OnTransform += LocationClaimsProvider.AddClaims ; app . UseClaimsTransformation(transform . TransformAsync) ; Класс ClaimsTransformer предоставляет событие OnTransform, которое будет обра­ щаться к мно жеству методов по мере получения запросов.
Глава 30. Расширенные средства ASP.NET Core ldentity 939 Каждый раз , когда поступает запрос, промежуточное ПО трансформации заявок вызывает метод Loca tionClaimsProvider. AddClaims (), который эмулирует ис­ точник данных человеческих ресурсов и создает специальные заявки. Для просмотра специальных заявок запустите приложение, войдите от имени разрешенного пользо ­ вателя и запросите URL вида /C laim. На рис. 30.5 показаны заявки для пользователя Alice. Можете выйти и снова зайти, чтобы понаблюдать за изменениями. Subjec1 Jss uer Туре Value Alice LOCAL AUTHORIТY SecuritySlamp ebbd9 127-5bbe-49f2-80ce-00036076690d Alice LOCAL AUTHORIТY Role Users Al ice LOCAL AUTHORIТY Role Em ployees 1 Alice LOCAL AUTHORIТY Name Alice 1 Alice LOCAL AU THORIТY Name ldentifier 9787а 1Ьа-6726 -4762 -ас7d-57еЗеЗе9dс9с 1 Alice RemoteClaims PostalC ode ос 20500 1 Alice RemoteClaims StaleOrProvince ос 11 1 [_ Рис . 30.5. Определение дополнительных заявок для пользователей Получение заявок из множества местоположений означает, что приложение не обя зан о дублировать данные , которые хранятся где-то в другом месте, и делает воз­ можной интеграцию данных от внешних участников. Свойство Cl aim. Issuer сооб­ щает, откуда происходит заявка, что помогает судить о том, насколько точными веро­ ятно будут данные, и какой вес они должны иметь в приложении . К примеру. данные о месте жительства, полученные из центральной базы данных человеческих ресурсов, скорее всего, будут более точными и надежными, чем данные, которые получены от внешнего поставщика списков рассьmки. Создание специальных заявок удостоверений Если в приложение необходимо добавить специальные локальные заявки, то это можно делать при создании новых пользователей . Класс UserManager<T> предлагает методы AddC laimAsync () и AddC laimsAsync (), предназначенные для определения локальных заявок, которые затем хранятся в базе данных и автоматически извлекаются, когда пользо­ ватель аутентифицирован (т. е . нет нужды полагаться на средство трансформации заявок) . Однако перед применением упомянутых методов следует обдумать, каким образом хранящи­ еся данные будут поддержи ваться в актуальном состоянии, и не лучше ли, чтобы приложение обслужи валось путем динамического извлечения данных из их источника. Как объясняется в следующем разделе, заявки используются при проверках авторизации, и устаревшие данные заявок могут предоставить пользователям доступ к частям приложения, к которым он должен быть запрещен, и предотвратить доступ к областям, к которым он был разрешен .
940 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC Использование политик Имеющи еся рабочие заявки можно применять для управления доступом пользо­ вателей к приложению бол ее гибким образом, ч е м с помощью стандартных рол е й. Пробл е ма с ролями в том, что они статичны , и после того, как пользователю была на­ значена роль, он остается ее членом до тех пор, поr<а не будет явно удален. Вот почему длительно работающие сотрудники крупных корпораций в итоге обладают букв ал ьно немыслимым доступом к внутренним системам: им назначаются роли, требуемы е для выполнения каждого нового задания, но старые роли удаляются р едко. Заявки используются для построения политик авторизации , которые являются частью конфигурации приложения и применяются к методам действий или контрол ­ лерам посредством атрибута Authori ze . В листинге 30.9 представлена простая по ­ литика, которая разрешает доступ только пользователям со специфическим типом и значением з аявки . Листинг 30.9 . Создание политики авторизации в файле Startup. cs using Micros o ft . AspNetCore . Builde r; u sing Microsoft . Ext e n s ions . Dependencyinjection ; using Microsoft . Ex t ensions . Con f iguration ; using Microsoft . AspNetCore . Hosting ; using Mi crosoft.Ent i tyFrameworkCore; u sing Mi crosoft. AspNetCore . I d e n tity. EntityFrame workCore; using Users.Models; u sing Users . Infrast r ucture ; using Microsoft.AspNetCor e .Identity ; using System.Security.Claims; namespace Users { pu Ыic class Start up I Con f igurat i onRoot Co nf igurat ion; puЫic Startup(IHos tingEnvi ronme nt env) { Configuration = ne w Config u rat i onBuilder() . SetBasePath(en v . Co n te ntRootPa t h) .AddJsonFile( "app setti ngs . json" ) .Build() ; puЬlic voi d Confi gur eServices (IServi ceCollection servi ces) services . AddTran s ient<IPasswordValidator<AppUs e r> , Cu s tomPasswo r dValidato r >() ; se r v i ces . AddTran sie n t<IUserVal i dator<AppUse r > , Cu stomUse r Validator>( ) ; services .AddAuthorization (opts => { opts. AddPolicy ( "DCUsers", policy => { policy.RequireRole("Users"); policy.RequireClaim(ClaimTypes.StateOrProvince, "DC"); }); }); s e rvi ces . AddDbContex t <Appide n t ityDbConte x t >(options => opt ions . UseSqlServer ( Configurati on["Data : Sp o r tStor e ide n t ity:ConnectionString" ])) ; s ervices . Addiden t ity<AppUser , Iden tit yRol e >(op t s => { opts . User . RequireUniqueEmai l = tru e ;
Глава 30. Расширенные средства ASP.NET Саге ldentity 941 opts . Password . RequiredLength = 6 ; opts . Password . RequireNonAlphanumeric = false ; opts . Password .RequireLowercase = false ; opts . Password .Requir eUppercase = false ; opts . Password . RequireDigit = false; }) . AddEntityFrameworkStores<AppidentityDbContext>(}; services . AddMvc() ; puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app . Useidentity() ; app.UseClaimsTransformation(LocationClaimsProvider.AddClaims) ; app.UseMvcWithDefau l tRoute(); AppidentityDbContext . CreateAdminAccount(app . ApplicationServices , Configuration) . Wait() ; Метод AddAuthorizat i on () настраивает политику авторизации и предоставляет экземпляр класса AuthorizationOptions , в котором определе ны члены , описанные в табл. 30.7 . Таблица 30.7. Члены класса AuthorizationOptions Имя DefaultPolicy AddPolicy(name , expression) Описание Это свойство возвращает стандартную политику авторизации, которая используется, когда атрибут Authorize был применен без каких-либо аргументов. По умолчанию политика проверяет, что пользователи прошли аутентификацию Этот метод используется для определения новой политики, как объясняется ниже Политики определяются с применением метода AddPolicy () ,который р або тает с лямбда-выражением, оперирующим на объекте AuthorizationPolicyBuilder для построения политики с помощью методов из табл. 30.8 . Таблица 30.8. Избранные методы класса AuthorizationPolicyBuilder Имя RequireAuthenticatedUser() RequireUserName(name) RequireClairn(type) Описание Этот метод требует, чтобы запрос был ассоциирован с аутентифицированным пользователем Этот метод требует, чтобы запрос был ассоциирован с указанным пользователем Этот метод требует, чтобы пользователь имел заявку указанного типа . Проверяется только присутствие за­ явки и принимается любое значение
942 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Соге MVC Имя RequireClaim(type , values) Requ ireRole(rol es) AddRequirements(requirement) Окончание табл. 30.8 Описание Этот метод требует, чтобы пользователь имел заяв­ ку указанного типа с одним из диапазона значений. Значения могут быть выражены как аргументы, разде­ ляемые запятыми, или в виде экземпляра реализации IEnumeraЫe<string> Этот метод требует, чтобы пользователь имел членство в роли. Множество ролей может быть указано как аргу­ менты, разделяемые запятыми, или в виде экземпляра реализации IEnumeraЬle<string>, и членство в лю­ бой роли обеспечит удовлетворение требования Этот метод добавляет к политике специальное требо­ вание, как описано в разделе " Создание специальных требований политики " далее в главе Политика в листинге 30.9 требует, чтобы пользователь имел членство в роли Users и заявку StateOrProvince со значением ос. Когда требований н еск олько, тогда все они должны быть удовлетворены, чтобы авторизация была одобрена. П ервый аргумент метода AddPo li cy () - это имя. посредством которого можн о ссылаться на политику при ее применении. Именем политики из листинга 30.9 явля­ ется DCUsers, и оно используется в атрибуте Authorize для применения политики к контроллеру Ноте в листинге 30.10. Листинг 30.1 О. Применение политики авторизации в файле HomeController. cs using System . Collections . Generic ; using Microsoft.AspNetCore . Mvc; using Microsoft.AspNetCore . Authorization; using Users . Models ; using Microsoft . AspNetCore.Identity ; using System .Thr eading . Tasks ; using System . ComponentModel . DataAnnotations ; namespace Users . Controllers { puЫic class HomeController : Controller { private UserManager<AppUser> userManager; puЫic HomeController(UserManager<AppUser> userMgr) { userManager = userMgr ; [Authorize] puЫic IActionResult Index() => View(GetData(nameof(Index))) ; / / [Authorize (Roles = 11 Users 11 ) ] [Authorize(Policy = "DCUsers")] puЫic IActionResul t OtherAction () => View ( 11 Index 11 , GetData(nameof(OtherAction))) ; 11 .. . для краткости другие методы не показаны ... private Task<AppUser> CurrentUser => userManager . FindByNameAsync(HttpContext.User . Identity . Name) ;
Глава 30. Расширенные средства ASP.NET Core ldentity 943 Свойство Policy используется для указания имени политики, которая будет при­ меняться, чтобы защитить метод действия. Результатом окажется комбинированная проверка имеющихся у пользователя ролей и заявок, которая выполняется, когда за­ прос нацелен на метод OtherAction ().Правильным сочетанием членства в ролях и заявок располагает только учетная запись Alice, в чем можно удостовериться, за­ пустив приложение, войдя от имени разных пользователей и запрашивая URL вида /Horne/OtherAction. Создание специальных требований политики Встроенные требования проверяют специфические значения, которые являются хорошей отправной точкой, но не позволяют обрабатывать каждый конкретный сце­ нарий авторизации. Например, если доступ должен быть запрещен для определенно­ го значения заявки, тогда использование встроенных требований сопряжено со слож­ ностями из-за того, что они попросту не созданы для проверки такого рода. К счастью, систему политик можно расширять специальными требованиями, которые представляют собой классы, реализующие интерфейс IAuthorizationRequirernent , и специальными обработчиками авторизации, которые являются подклассами класса AuthorizationHandler и занимаются оценкой требования для заданного запроса. Добавьте в папку Infrastructure файл класса по имени BlockUsersRequirernent. cs, после чего определите в нем специальное требование и обработчик (листинг 30.11). Листинг 30.11. Содержимое файла BlockUsersRequirement. cs из папки Infrastructure using System ; using System.Linq; using System . Threading . Tasks; using Microsoft.AspNetCore.Authorization ; namespace Users . Infrastructure { puЫic class BlockUsersRequirement : IAuthorizationRequirement puЬlic BlockUsersRequirernent(pararns string[J users) { BlockedUsers = users; puЫic string [] BlockedUsers { get ; set ; } puЫic class BlockUsersHandler : AuthorizationHandler<BlockUsersRequirement> protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, BlockUsersRequirement requirement) if (context . User.Identity != null && context . User . Identity . Name != null && !requirement.BlockedUsers . Any(user => user . Equals(context .U ser .I dentity . Name , StringComparison.OrdinalignoreCase) )) { context.Succeed(requirement); else { context . Fail() ; return Task . CompletedTask ;
944 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC Кл асс BlockU s e r Requ irement представляет требование и применя ет ся для ука ­ з ания данных, исполь зуемых при создании политики; в рассматрива ем ом случа е э то списо1< пользователей , которые не будут авторизованы . Класс BlockUsersHandler ответств енен за оцеюч запроса на авторизацию с применением данных тр е бования и унаследован от класса Authorizat i o nH a n d l er<T> , где Т - тип класса тр ебования. Метод Handl e () вызывается для класса обр аботчика , когда система авторизации нужда ется в проверке доступа 1< ресурсу. В качестве аргументов методу п еред аются объект Au t h or i zationHand l erCon t ext , который определяет члены, опи с анные в табл. 30.9, и объект требования, который предоставляет доступ к данным, необходи­ мым для выполнения проверки . Таблица 30.9. Избранные члены класса AuthorizationHandlerContext Имя User Succeed(requirement) Fail () Resou rce Описание Это свойство возвращает объект ClaimsPr i n c i pal , ассоциированный с запросом Этот метод вызывается , если запрос удовлетворяет требованию. Аргументом является объе кт реализации I Au tho rizati onRequi rement , получаемый методом Handl e () Этот метод вызывается, если запрос не удовлетворяет требованию Это свойство возвращает объект, который используется для авторизации доступа к одиночному ресурсу прило жения , как объясняется в разделе "Использование политик для авторизации доступа к ресурсам" далее в главе Обр аботчик требования в листинге 30. l l проверяет имя пол ьзов ателя. чтобы выяснить, находится ли оно в списке запрещенных пользователей , которы й пре­ доставляет объект BlockUsersRequ i remen t, и вызывает метод Succeed () ил и Fail () соответственно. Применение специального требования сопряжено с двумя и зменениями конфигурации (листинг 30.12) . Листинг 30.12 . Применение специального требования авторизации в файле Startup. cs using Mi cro s o f t . AspNetCore . Bu ilder; using Mi crosoft .Extensions . Dependencyinjecti on; us i ng Microsoft.Ext ens i o n s . Configur ation; using Microsof t . As p NetCore .H ost ing ; u sing Microsof t . Ent i tyFrameworkCor e ; usin g Microsoft. As p Ne t Co re.Ident ity . Enti tyFrameworkCore ; using Users.Model s; us ing Users . I n frastructure ; usi n g Micro s o ft. AspNetCore .Identi ty; using System.Security.Claims ; using Microsoft.AspNetCore.Authorization; namespace Use r s { puЬlic cl ass Startup IConfigu r at ionRoot Configurati on;
Глава 30. Расширенные средства ASP.NET Core ld entity 945 puЫic Startup(IHostingEnvironrnent env) { Configuration = new ConfigurationBui l der() . SetBasePath(env . ContentRootPath) .Ad dJsonFile( " appsettings .js on ") .Build() ; puЫic void ConfigureServices(IServi ceCollection services) services . AddTransient<IPasswordValidator<AppUser> , CustomPasswordVa lida tor>() ; services . AddTransient<IUserValidator<AppUser> , CustornUserValidator>(); services.AddTransient<IAuthorizationHandler, BlockUsersHandler>(); services . AddAuthorization(opts => { opts . AddPolicy( " DCUsers ", policy => { policy . RequireRole( " Users "); policy . RequireClaim(C la imTypes . StateOrProvince , "ОС"); }); opts. AddPolicy ( "NotBob" , pol.icy => { policy.RequireAuthenticatedUser(); policy.AddRequirements(new BlockUsersRequirement("Bob")); }); )); services.AddDbContext<AppidentityDbContext>(options => opt i ons . UseSqlServer( Configuration[ "Da ta : SportStoreidentity :Conn ectionString " ] )); services.Addidentity<AppUser , IdentityRole>(opts => { opts . User .RequireUnique Em ail = true; opts.Password.RequiredLength = б; opts . Password.Req uireN onAlphanurneric = false ; opts . Password .RequireLowercase = false; opts . Password .Requ ir eUppercase = false; opts . Password.RequireDigit = false; )) . AddEntityFrameworkStores<AppidentityDbContext>() ; services . AddMvc() ; puЬlic void Configur e(IAppli cationBuilder арр) { app . UseStatusCodePages() ; app . UseDeveloperExceptionPage() ; app.UseStaticFiles() ; ap p.Useide ntity(); app .Us eClaimsTransformation(LocationClaimsProvider . AddClairns) ; app . UseMvcWithDefaultRoute() ; AppidentityDbContext . CreateAdrninAccount(app.ApplicationServices, Configuration) .Wai t() ; Первый шаг предусматривает регистрацию 1<ласса обработчика с помощью пос­ тавщика служб как реализации интерфейса IAuthorizationHandler. Второй шаг предполагает добавление к политике специального требования, что делается с ис­ пользованием метода AddRequ irements () :
946 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC opts .AddPolicy("NotBob" , policy => { policy . RequireAuthenticatedUser(); policy.AddRequirements(new BlockUsersRequirement("Bob")); }); Результатом будет политика, которая требует аутентифицированных пользовате­ лей, отличных от ВоЬ, и может применяться посредством атрибута Authorize сука­ занием имени политики (листинг 30.13). Листинг 30.13. Применение специальной политики в файле HomeController. cs 11 [Authorize(Roles = "Users")] [Authorize (Policy = "DCUsers")] puЫic IActionResult OtherAction() => View("Index", GetData(nameof(OtherAction))); [Authorize (Policy = "NotBob") ] puЫic IActionResult NotBob () => View ("Index", GetData (nameof (NotBob))) ; В случае аутентификации 1<ак пользователя ВоЬ вы не сможете получить до­ ступ к URL вида /Home/NotBob, но всем остальным учетным записям доступ будет предоставлен. Использование политик для авторизации доступа к ресурсам Политики могут также применяться для контроля доступа к индивидуальным ре­ сурсам, что представляет собой общий термин, означающий любой элемент данных, который используется в приложении и требует более детализированного управления, чем возможно на уровне методов действий. Добавьте в папку Models файл по имени ProtectedDocument. cs и определите в нем класс, который представляет документ с рядом свойств, касающихся владения (листинг 30.14). Листинг 30.14. Содержимое файла ProtectedDocumen t. cs из папки Models namespace Users.Models { puЬlic class ProtectedDocument puЬlic string Title { get; set; } puЫic string Author { get; set; } puЫic string Editor ( get; set; } Это всего лишь заполнитель для реального до1<умента, а его ключевой аспект в том, что править каждый д01<умент разрешено только двум лицам: автору и редакто­ ру. Реальный документ потребовал бы содержимого, хронологии изменений и многих других средств, но для примера вполне достаточно и такого простого класса. Добавьте в папку Controllers файл класса по имени DocumentController. cs и создайте в нем контроллер, как показано в листинге 30.15.
Глава 30 . Расширенные средства ASP.NET Core ldentity 947 Л истин г 30.15. Содержи м ое фа й ла DocumentController. cs из папки Controllers using Microsoft.AspNetCore . Authorizat i on ; using Microsoft . AspNetCore . Mvc; using System . Linq; using Users . Models ; namespace Users.Controllers [Authorize] puЬlic class DocumentController : Controller { private ProtectedDocument[J docs = n ew Pro t ectedDocument[J new ProtectedDocument { Title " QЗ Budget ", Author = " Alice ", Editor = "Joe" }, new ProtectedDocument Editor = "Alice" } Titl e "Proj ect Plan", Author = "ВоЬ", }; puЫic ViewResult I ndex() => View(doc s ) ; puЬlic ViewResult Edit(string title) { return View("Index ", docs . First OrDef ault (d => d . Titl e title)) ; Контроллер поддерживает фиксированный набор объектов ProtectedDocument. Объе1пы ProtectedDocument при м еняются в де йствии I ndex, п е р едающе м вс е до­ кументы методу Vi ew ( ) , и в действии Edit , которое выбирает один документ на основе аргумента title. Об а м етода действий используют представление по имени Index . chstml, по этому создайт е новую папку Views/Document и пом е стите в н ее ф айл Index . chstml с ра змет кой из листинга 30.16. Листинг 30. 16. Содержимое файла Index. cshtml из пап к и Views/Docurnent @if (Model is IEnumeraЫe<ProtectedDo c u ment>) { <div class= " bg- p rimary panel-body"> <h4>Documents (@User? .Identity? .N a me)</ h 4> </div> <tаЫе class= " taЫe taЫe - condensed t a Ьl e - bordered " > <tr><th>Title</th><th>Author</th><th>Editor</th><th></th></tr> @foreach (var doc in Model) { <tr> <td>@doc . Title</td> <td>@doc . Author</td> <td>@doc . Editor</ t d> <td> <а class="btn btn - sm btn - primary " asp - action= " Edit" asp-route-title= " @doc . Ti t l e " > Edit </а> </td> </tr> } </tаЫе> else { <div class= " bg - prirnary panel- body " >
948 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC <h4>Editing @Model . Title (@User? .Identity? . Na me) </h4 > </ div> <div class= " panel-body"> Document editing feature would go here ... </div> < а asp-action="Index" class="Ьtn Ьtn-primary" > D o ne < /a > <а asp-action="Logout" asp-controller="Account" class= " btn btn-danger">Logout</a> Если модель представления - последовательность объектов ProtectedDocument, тогда представление отображает таблицу, в которой для каждого документа предус­ мотрена строка с именами автора и редактора, а также ссылкой на действие Edi t. Если модель представления - одиночный объект ProtectedDocument, тогда пред­ ставление отобразит заполнитель, на месте которого реальное приложение предло­ жило бы средства редактирования . В настоящий момент единственным ограничением авторизации является атри­ бут Authorize, примененный к классу DocumentController, который означает, что любой документ может редактировать любой пользователь, а не только автор или редактор . В этом легко убедиться, запустив приложение, запросив URL вида /Document, войдя от имени любого пользователя приложения и щелкая на кнопках Edit (Редактиров ать ) для документов. На рис. 30.6 иллюстрируется редактирование документа Project Plan пользователем Joe. Title Author 1 18 i Documen t ediHng feature \•1ould go t1ere" . 1 аз Budgel Alico Joe ВоЬ Allce 111 11111181 L___т·-------"-·---·----~ _J Рис. 30.6. Редактирование документов Создание политики и обработчика авторизации доступа к ресурсам Ограничение доступа к индивидуальным документам на уровне методов действий обеспечить трудно, потому что атрибут Authorize оценивается перед вызовом метода действия. Другими словами, решение относительно авторизации принимается до того, как объект ProtectedDocument извлечен и может быть проинспектирован с целью выяснения, каким пользователям должен быть предоставлен доступ к документу. Пробле ма решается созданием политики и обработчика авторизации, которым из­ вестно, каким образом иметь дело с объектами ProtectedDocume nt, и их использо­ ванием внутри метода действия после того, как были выявлены детали о пользовате­ ле. Добавьте в папку Infrastructure файл по имени DocumentAuthorization. cs и определите в нем классы, представленные в листинге 30.17.
Глава 30. Расширенные средства ASP.NET Core ldentity 949 Ли сти нг 30.1 7 . С одер жим ое файла Documen tAu thori zation. cs из папки Infras t r uctur e using System ; using System . Threading . Tasks ; using Microsoft . AspNetCore . Authori zat i on ; using Users . Models ; namespace Users.Infrastructure puЫic class DocumentAuthorizationRequirement IAuthorizationRequirement { puЫic bool AllowAuthors { get ; set ; puЬlic bool AllowEditors { get ; set ; puЫic class DocumentAuthorizationHandler : AuthorizationHandler<DocumentAuthorization Requirement> protected override Task HandleRequirementAsync( AuthorizationHandlerContext context , DocumentAuthorization Requirement requirement) ProtectedDocument doc = context . Resource as ProtectedDocument ; string user = context . User . Identity . Name ; StringComparison compare = St ringComparison . OrdinalignoreCase ; if (doc != null && user != null && (requirement.AllowAuthors && doc . Author . Equa l s(use r, compare)) 11 (requirement . AllowEditors && doc . Editor . Equals(user , compa r e) )) context . Succeed(requirement) ; else { context . Fail() ; return Task . CompletedTask ; Объ ект AuthorizationHandlerContext пр едлагает свойство Resource , об е спе­ чивающее до ступ к объекту, который может быть проинспектирован для автори зации. Кл асс DocumentAuthorizationHandl er проверяе т, содержи т л и сво й ство Resource объект ProtectedDocument , и если это так, тогда выясняет, является ли текущий пользовател ь автором или редактором , а также разрешает ли объект DocumentAutho rizationRequirement авторам или ред акторам иметь доступ к докуме нту. В ли стинг е 30 . 18 кл асс DocumentAuthorizationHandler р е гистрируется в ка­ честве о б работчи ка для тр еб ований DocumentAuthorizationRequirement и опре­ деляется политика , которая имеет это требование. Листинг 30. 18. Регистрация обработчи ка и определение политики в файле Startup. cs puЫic void ConfigureServices(ISe r viceCol l ection services) services . AddTransient<IPasswordValidator<AppUser> , CustomPasswordValidator>() ; services.AddTransient<IUserVal idator<AppUser> , CustomUserValidator>() ; services . AddTransient<IAuthorizationHandl er , BlockUsersHandler>() ;
950 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC services.AddTransient<IAuthorizationHandler, DocumentAuthorizationHandler>(); services . AddAuthorization(opts => { opts . AddPo l icy( " DCUsers ", policy => { po li cy . RequireRole("Users "); policy . RequireClaim(ClaimTypes.StateOrProvince , "DC"); }); opts . AddPolicy ( " NotBob ", policy => { policy .Requir eAuthenticatedUser() ; policy.AddRequirements(new BlockUsersRequirement( " Bob " )); }); opts. AddPolicy ( "Au thorsAnd.Edi tors" , policy => { policy.AddRequirements(new DocumentAuthorizationRequirement AllowAuthors = true, AllowEditors = true }); }); }); services . AddDbCont ext<App identityDbContext>(opt i ons => options . UseSqlServer( Configuration[ "Data : SportStoreidentity : Connect i onString " ]} ) ; services . Add identity<AppUser , Identit yRole>(opts => { opts . User .RequireUniqueEmail = true; opts .Password . RequiredLength = 6 ; opts . Password . RequireNonAlphanumeric = fa l se ; opts . Password .RequireLowercase = false ; opts . Password . RequireUppercase = false ; opts . Password . RequireDigit = false; }) . AddEntityFrameworkStores<Appident i tyDbContext>() ; servi ces .AddMvc (); Финальный шаг связан с применением политики авторизации в методе действия (ли стинг 30. 19). Листинг 30.19. Применение политики авторизации в файле DocumentController. cs using Microsoft . AspNetCore . Authorization ; using Microsoft . AspNetCore.Mvc ; using S ys tem.Linq ; using Users.Models ; using System.Threading.Tasks; namespace Users.Controllers { [Authorize ] puЫic class DocumentContro ll er : Controller { private ProtectedDocument[] docs = new ProtectedDocument[] }; new ProtectedDocument { Ti tle " QЗ Budget ", Author = " Al ice ", Editor = "Joe" }, new ProtectedDocument Editor = "Alice" } Title "Project Plan ", Author = " ВоЬ ", private IAuthorizationService authService;
Глава 30 . Расширенные средства ASP.NET Саге ldentity 951 puЫic DocumentController(IAuthorizationService auth) { authService = auth; puЬlic ViewResult I ndex() = > Vi ew(doc s ) ; puЫic async Task<IActionResult> Edit{string title) { Protecte dDocument doc = docs.FirstOrDefault(d => d.Title bool authorized = await authService.AuthorizeAsync(User, doc, "AuthorsAndEdi tors") ; if (authorized) { return View { 11 Index 11 , doc); else { return new ChallengeResult{); title); Конструктор контроллера определяет аргумент IAut h or iz ation Se r vice , кото­ рый предоставляет методы. предназначенные для оценки политик авторизации, и распознается с использованием внедрения зависимостей . В методе E d i t () вызы­ вается метод AuthorizeA s y n c () с передачей ему текущего польз ователя. объек­ та ProtectedDocume nt и имени политики . подлежащей применению. Если метод AuthorizeAsyn c () в результате возвращает t rue , то авторизация одобрена и вы­ зывается м етод Vi ew () . Если результатом оказьmается false , тогда есть проблема с авторизацией. и возвращается объект Chal l e ngeResu l t , описанный в главе 17, ко­ торый сообщает инфраструктуре MVC о том, что произошел отказ в авторизации. Чтобы посмотреть на эффект. запустите приложение и запрашивайте URL вида /Docume n t , аутентифицируясь от имени разных пользователей . Если, например , вы аутентифицируетесь как Joe , то получите возможность редактировать документ QЗ Budget, но не документ Pro j ect Plan. Использование сторо н ней аутентификации Одно из преимуществ системы на основе заявок, такой как ASP.NEТ Саге Identity. свя­ зано с тем, что заявки могут поступать из внешней системы, причем даже те, которые идентифицируют пользователя для приложения. Это означает, что другие системы могут аутентифицировать пользователя от имени приложения, и система ASP.NEТ Саге Identity построена с учетом такой идеи, чтобы упростить добавление поддержки для аутентифика­ ции пользователей через третьи стороны, вюпочая Microsoft, Google , Facebook и 1Witter. Прим енение сторонней аутентификации обеспечивает важные преимущества: многие пользователи уже имеют учетные записи , пользователи получают возмож­ ность выбирать исполь зование двухфакторной аутентификации и отсутствует пот­ ребность в управлении пользовательскими учетными данными внутри приложения. В последующих разделах будет показано, как настроить и задействовать стороннюю аутентификацию для пользователей Googl e . Регистрация приложения в Google Службы сторонней аутентификации обычно требуют рег истрации прилож ений перед тем. как они смогут аутентифицировать пользователей . Результатом процесса регист­ рации будут учетные данные, которые включаются в запрос аутентификации к сторон­ ней службе. Процесс регистрации Google инициируется по адресу h t tp : / / console .
952 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC d e vel o pe r s. g oogl e . сот и выполняется согласно инструкциям , доступным по ссылке h t tp : //deve l opers . google . com /identity/sign- in/weЬ/devconsole - pro j ect. Понадобится указать URL обратного вызова, который для ст андартной конфигурации выглядит как / signin - google . В среде разработ1ш URL обратного вызова должен быть установлен в http : / /localhos t: пopт/signin - google. Для прои зводств е нно­ го приложения необходимо создать URL. который включает общедоступно е имя хоста и порт. После прохождения процесса регистрации будет получен идентификатор кли ента , идентифицирующий приложение в Goog\e, и секретный код клиента, который при­ меняется в качестве меры безопасности, не позволяя другим приложениям выдав ать себя за ваше приложение . На заметку! Вы должны зарегистрировать собственное приложение и использовать иденти­ фикатор и секретный код клиента, сгенерированные процессом регистрации . Код в на­ стоящем разделе не будет работать до тех пор, пока вы не измените учетные данные на значения, уникальные для вашего приложения. Компания Microsoft предлагает средство под названием пользовательские секретные коды, которое позволяет хранить секретную информацию за пределами конфигурационного файла, но ради простоты предполагается, что конфигурационные файлы не доступны публично и могут безопасно содержать учет­ ные данные аутентификации Google. Включение аутентификации Google Система ASP.NET Core Identity располагает встроенной подде ржкой для аутенти­ фикации пользователей с помощью их учетных записей Microsoft, Google , Facebook и Тwitter, а также общую поддержку для любой службы аутентификации. которая ра­ бота ет по протоколу OAuth. Добавьте в файл pro j ect . j son пак ет NuGet, который включает специфичные для Google дополнения к системе ASP.NET Core Ideпtity (лис­ тинг 30.20) . Листинг 30. 20. Добавление пакета NuGet в файле proj ect. j son "dependencie s ": { "Microsoft.NETCore .App": "version": "1 .0 .0", " type ": " platform " }, " Mic r osoft . AspNetCore .D iagnostics": "1. 0 . 0 ", " Mic r osoft . AspNetCore . Server . IISintegra t ion ": "1. 0 . О ", " Microsoft . AspNetCore . Server . Kestrel ": " 1 . 0 . 0 ", "Microsoft .Extensions . Logging .Console": "1 .0 .0", " Microsoft. AspNetCore . Mvc " : "1. О . О ", " Microsoft . AspNetCore . St a t ic Files ": " 1.0 .0", " Microsoft . AspNetCore . Razor . Tools ": }' "version": "1 . О . 0-preview2-final ", "type": "build" " Mic r osoft . Exten sions . Configura t ion": "1 .0 .0", " Microsoft . Extensions . Configurati o n. Json ": "1.0 .0", " Microsoft . AspNetCore . Identity . Entity FrameworkCore ": " 1.0 . 0 ",
Глава 30 . Расширенные средства ASP.NET Core ldentity 953 " Microsoft . EntityFrameworkCore . Sql Se rver ": " 1 . 0 . 0 ", " Microsoft . EntityFrameworkCore . Tools ": "1.0. 0 - preview2 -fina l", "Microsoft.AspNetCore.Authentication.Google": "1.0.0" }, Для каждой службы, поддерживаемой ASP.NET Core Identity, предусмотрен свой па­ кет NuGet, как показано в табл. 30.10. Таблица 30.1 О. Пакеты NuGet для аутентификации Имя Microsoft . AspNetCore .Authentication.Googl e Microsoft . AspNetCore .Authent i cati on .Facebook Microsoft . AspNetCore . Authentication .T wi tter Mi crosoft . AspNetCore . Authentication . Microsof tAccount Microsoft . AspNetCore . Authentication . OAuth Описание Аутентифицирует пользо­ вателей с применением учетных записей Google Аутентифицирует пользо­ вателей с использованием учетных записей Facebook Аутентифицирует пользо­ вателей с применением учетных записей Twitter Аутентифицирует пользо­ вателей с использованием учетных записей Microsoft Аутентифицирует пользо­ вателей с помощью любой службы OAuth 2.0 Каждый пакет имеет метод для добавления промежуточного ПО в конвейер обра­ ботки запросов, так что он может перехватывать и перенаправлять запросы аутенти­ фикации. В листинге 30.21 вызывается метод, который настраивает службу аутенти­ фикации Goo gle . Листинг 30.21 . Включение аутентификации Google в файле Startup. cs puЫic void Configure(IApplicationBuilder арр) { app.UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app.Useidentity() ; app.UseGoogleAuthentication(new GoogleOptions Clientid = "<enter client id here>", ClientSecret = "<enter client secret here>" }); app.UseClaimsTransformation(LocationC l a imsP rovider . AddC l aims) ; app . UseMvcWithDefaultRoute(); AppidentityDbContext . CreateAdminAccount(app . ApplicationServices , Configuration) . Wait();
954 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Метод UseGoogl eAuthenticat i on () настраивает промежуточное ПО, тр е буемое для аутентификации пользов ателей с помощью Googl e, и ук аз ывает идентификатор и секр етный код клиента, которые были созданы в процессе регистрации. При аутентификации пользователя с участием третьей стороны можно выбрать создание пользоват еля в баз е данных I de ntit y, которая впоследствии буд ет при­ меняться для управления ролями и заявками как в случае обычных пользов ателей . В главе 28 был добавлен класс проверки пользователей , пр едотвращающий создание пользователя, если его адрес эл е1пронной почты не относится к домену example . com . Поскольку планируется иметь дело с пользователями из любого дом ена, проверку ад­ реса электронной почты для этого примера необходимо отключить (листинг 30.22). Листинг 30.22. Отключение проверки домена в файле CustornUserValidator. cs u s ing System . Collecti ons . Generic ; using Syst em.Linq; u si ng System.Th readi n g .T asks ; using Mi crosoft . AspNetCore .I dentity ; using Users . Models ; namespace Use r s .I n f rastructure puЬlic class CustomUserVal i dator : UserValidator<AppUser> { puЬl ic override async Task<Identit yResult> Va l idateAs ync( UserManager<AppUser> manager , AppUser user) { I dentityResul t r esult = await ba s e . Va lidateAsync(ma n ager , user) ; Li s t<IdentityEr r or> e r ro r s = r esult.Succeeded ? new List< I dentityError>() : result .E rrors . ToList() ; // if ( !user. Email. ToLower () . EndsWith ("@example.com")) / / errors. Add (new IdentityError { 11 11 11 11 Code = "EmailDomainError", Description = "Only exarnple. com email addresses are allowed" }); ret u rn errors . Count ==О ? Identi t yRe s ult . Success : I dentityResult. Failed(errors . ToAr ray() ); Далее нужно добавить в файл пр едставл ения View s /Account/Login . cshtml кнопку. которая по зволит пользоват елям входить ч е р е з Google (листинг 30.23). Для кнопок в Google предусмотрены изображения, чтобы обеспечить согласованный вне ­ шний вид с остальными пр ил ожениями, поддержив ающими учетны е з аписи Google , но ради простоты достаточно создать стандартную кнопку. Листинг 30.23. Добавление кнопки в файле Login. cshtml из папки Views/Account @model LoginModel <di v cl ass= " bg-primary panel-body"><h 4>Log In</h4></div> <div cla s s= " text-danger " asp - validation -s ummary= " All " ></div>
Глав а 30. Расширенные средства ASP.NET Соге ldentity 955 <form asp - action= " Login " method= " post " > <input type= " hidden " name= " returnUrl " value= " @ViewBag . return Ur l" /> <div class= " form - group " > <label asp - for= " Email " ></label> <input asp - for= " Email " class= " form - control " /> </div> <div class=" form- group"> <label asp - for= " Password " ></label> <input asp - for= " Password " class= " form - control " /> </div> <button class= " btn btn- pri mary" t ype= " submit" >Log In</b ut ton> < а class="Ьtn Ьtn- i nfo" asp-action="GoogleLogin" asp-route-returnUr1="@ViewBag.returnUr1">Log In With Goog1e </а> </form> Н ов ая кнопк а нацелена на действие GoogleLogi n контролл ера Account . В лис ­ тинге 30.24 приведен код метода действия GoogleLogin () и другие изменения, вне­ сенные в контроллер. Л ист и нг 30.24. Добавление поддержки аутентификации Google в файле AccountController. cs using System . Threading . Tasks ; using Microsoft . AspNetCore . Authorization ; using Microsoft.AspNetCore . Mvc ; using Users.Models ; using Microsoft . AspNetCore . Iden tity ; using System . Security.C1aims; using Microsoft.AspNetCore .Http.Authentication; namespace Users.Controllers { [Authorize] puЫic class AccountController : Controller { private UserManager<AppUser> userManager ; private SigninManager<AppUser> s i gninManager ; / / ... для краткости другие методы не показаны . . . [AllowAnonymous] puЬ1ic IActionResu1t Goog1eLogin(string returnUr1) string r edirectUrl = Url. Action ( "GoogleResponse", "Account", new { ReturnUrl = r e turnUrl }) ; AuthenticationProperties properties = signinМanager . ConfiqureEx ternalAuthenticationProperties("Google", redirectUrl); return new ChallengeResult("Google", properties); [AllowAnonymous] puЫic async Task<IActionResult> GoogleResponse (string returnUrl = "/") { ExternalLogininfo info = a wait signinМanager.GetExternalLogininfoAsync(); if (info == nu11) { r e tur n RedirectToAction(nameof(Login)); var resu1t = a wait signinМanager.Externa1LoginSigninAsync(
956 Часть 11. Подробные сведения об инфраструктуре ASP.N ET Core MVC info.LoginProvider, info.ProviderKey, false); if (result.Succeeded) { return Redirect(returnUrl); }else{ AppUser user = new AppUser { Email = info.Principal.FindFirst(ClaimTypes.Email) .Value, UserName = info.Principal.FindFirst(ClaimTypes.Email) .Value }; IdentityResult identResult = await userManager.CreateAsync(user); if (identResult .Succeeded) { identResult = await userManager.AddLoginAsync(user, info); if (identResult .Succeeded) { await signinМanager.SigninAsync(user, false); return Redirect(returnUrl); return AccessDenied(); Метод Goog leL ogin () создает экземпляр класса AuthenticationProperties и устанавливает свойство Redi rec t Ur i в URL, который нацелен на действие Goog leResponse того же контроллера. Следующая часть кода заставляет систему ASP.NEТ Core Id entity реагировать на отсутствие авторизации перенаправлением пользователя на страницу аутентификации Google вместо страницы, определенной в приложении: return new ChallengeResu lt ( "Google", properties) ; Это означает, что когда пользователь щелкает на кнопке Log ln With Google (Во йти с помощью Google), браузер перенаправляется на страницу аутентификации Google и затем после успешного прохождения аутентификации направляется обратно на метод действия GoogleResponse (). Внутри метода GoogleResponse () детали внешнего входа получаются вызовом метода GetExternalLogininfoAsync () объекта SigninManager: ExternalLogininfo info = await signinManage r . GetExternalLogininfoAsync () ; Класс ExternalLogininfo определяет свойство ExternalPrincipal, возвраща­ ющее объект ClairnsPrincipal, который содержит заявки, предоставленные пользо­ вателю механизмом Google. Для входа пользователя в приложение используется метод ExternalLoginSigninAsync() : var result = await sign!nManager.ExternalLoginSigninAsync( info . LoginProvider , info . ProviderKey, false); Если вход произвести не удалось, то причина в том. что в базе данных отсутствует запись , которая представляла бы пользователя Google. Для решения проблемы с при­ менением следующих двух операторов создается новый пользователь и ассоциируется с учетными данными Google:
Глава 30. Расширенные средства ASP.NET Соге ldentity 957 IdentityResult i dentResu l t = await use r Ma nager .CreateAsync (u s er) ; identResult = await userMa nager .AddLoginAsync ( u ser , info) ; На заметку! При создании пользователя ldentity используется заявка адреса электронно й поч­ ты, предоставленная Google для свойств Email и Us e r Na me объе кта App Us er , та к что никаких конфликтов имен с пользователями , существующими в базе данных, не возникает. Чтобы протестировать аутентификацию, запустите приложение, щелкните на кнопке Log lп With Google и введите учетные данные для допустимой учетной запи­ си Goo gle. Когда процесс аутентификации завершится, браузер будет перенаправлен обратно на приложение. Перейдя на URL вида /Claim s, вы заметите, что заявки для пользователя , показьmающие выполнение аутентификации системой Google , были до­ бавл ены к удостоверению пользователя (рис. 30.7). D IJ~tfS - ·. "· ·.· .· • - ох с rС!?..~~~~h~т~~5!·;11~:~~~ -_ --=-~--~-- --=~~~-~~=:.~-: ~~~- ~- Subject lss uer Туре Value tesluser ~ gmail.com LOCAL AUTHORIТY SecurityStamp 6Ь2а4092·Ы2е·46f6·асЬ 1-5d01 aa5d8ae b Adarn Fr eeman Google Em ail 1esteru se r6 gmail .com Adam Fr ee man Google Give11Name Adam l es t нsor@g rn ai l . com LOCAL AUTHORITY Name testuser~g mail.com Ado.m Fre eman Google Nnm e Adain Free man 1es111ser ~ gmail.com LOCAL AUTHORIТY Nameldenlifier 0689е2сЗ-4с84·4dЬ2·91 cd·8141494a 6d96 AdamFreeman Googl e Name\de ntif ier 101888805463382593408 testu sor@ gma. il.com Remote ClaJms PostalCode NY 10036 tes iu sor6gma ll.c om RemoteClaims StaleOrProvlnce NY Adai11 Fteemд n Googl e Surna in e Free 1na11 ' -- - --~ - ---- - - J Рис . 30 .7. Пр име р исп ользов а ния сторонне й аутентификации Резюме В настоящей главе были описаны некоторые расширенные средства, поддержи­ ваемые системой ASP.NET Core Identlty. Здесь демонстрировалось применение специ­ альных свойств пользователя и обеспечение их поддержки за счет обновления схемы базы данных с использованием миграций базы данных. Далее в главе объяснялась работа заявок и их применение для создания более гибких способов авторизации пользователей посредством политик. Также было показано , как использовать поли­ тики для контроля доступа к индивидуальным ресурсам, управляемым приложением . В завершение главы рассматривалась задача аутентификации пользователей через Google , которая построена на ид ее, л ежашей в основе прим е нения заявок. В следую­ щей главе вы ознакомитесь с тем, как в действительности реализованы самые важ­ ные соглашения, используемые в приложениях МVС , и каким образом их можно на ­ страивать в собственных приложениях .
ГЛАВА 31 Соглашения по модели v v и ограничения деиствии н а протяжении всей книги неоднократно подчеркивалось, что никакой магии с разработкой приложений МVС не связано, и беглый взгляд "за кулисы" позво­ ляет легко выявить, каким образом все части подгоняются друг к другу для доставки функциональности , которая была описана в предшествующих главах. В последней главе книги рассматриваются два полезных средства, позволяющие настраивать способ работы приложения МVС. Соглашения по модели дают возмож­ ность заменять соглашения, используемые при создании контроллеров и действий, с переопределением тех, которые применяются по умолчанию. Ограничения действий позволяют указывать, для какого вида запросов может использоваться то или иное действие. Они предоставляют инфраструктуре MVC инструкции относительно выбора действия, которое будет обрабатывать запрос. Если хотите, то можете не читать эту главу (из-за того, что местами материал ха­ рактеризуется довольно высокой сложностью}, тем не менее, имейте ее в виду, ког­ да приложение поведет себя не так, как бьmо задумано. Описанные в данной главе средства не придется задействовать особенно часто (или даже вообще никогда}, но чем больше вы узнаете о работе инфраструктуры МVС, тем лучше будете подготовле­ ны к решению возникающих проблем. В табл. 31.1 приведена сводка для настоящей главы. Таблица 31 .1. Сводка по главе Задача Настройка модели приложения Применение настройки повсюду в приложении Проведение различия между двумя методами действий, которые могли бы обрабатывать запрос Решение Используйте один из встроенных ат­ рибутов или создайте специальное соглашение по модели Определите глобальное соглашение по модели Используйте ограничения действий Листинг 31 .1 -31.15 31.16, 31.17 31.18-31 .26
Глава 31 . Соглашения по модели и ограничения действи й 959 Подготовка проекта для примера Создайт е новый проект типа Empty (Пустой) по имени Convent ionsAndCons t r а i n t s с ис пользованием шаблон а ASP.NET Core Web Application (.NET Core) (Веб-приложение ASP.NET Core (.NET Core)). Добавьте требуемые пакеты NuGet в раздел dep endencie s файла proj ect . j son и настройте инструментарий Razor в разделе tool s, как по­ казан о в л истинг е 31 . 1 . Разделы, которые не нужны для данной главы, понадобится удалить . Листинг 31 .1. Добавление пакетов в файле proj ect. j son " dependencies ": { "Microsoft .NETCore .App ": " version": "1.0 .0", " type" : "platform" }' }' " Microsoft . AspNetCore . Diagnostics ": "1. 0 . 0 ", "Microsoft .AspNetCor e . Server . IISintegration": "1 .0 . 0 ", "Microsoft .AspNetCore . Server. Kestrel ": "1 .0 .0 ", "Microsoft.Extensions.Logging.Console": " 1 . 0 . О", 11 Мicrosoft. AspNetCore. Mvc 11 : 11 1.О.О 11 , 11 Microsoft.AspNetCore.StaticFiles 11 : 11 1.0.0 11 , "Microsoft . AspNetCore.Razor.Tools": { 11 version": "1.0 .0 -preview2-final 11 , "type": "build" "tools ": }' "Microsoft.AspNetCore . Razor.Tools": 11 1.0 .0-preview2-final 11 , " Microsoft . AspNetCore . Server . IIS in tegration . Tools ": "1 .0 .0-preview2-final" " frameworks ": { " netcoreapp l. 0 ": " imports ": [ "dotnetS . 6 ", "portaЫe -net45+wi n8 "] }' " buildOptions ": " emitEntryPoint": true , " p r eser v e Comp i lationContext ": t rue }' " runtimeOpt i ons ": { " configPrope rties ": { "System . GC. Server ": true } В листинге 31.2 приведен код класса Sta rtup, который конфигурирует средства, пр едо ставляемые пакетами NuGet.
960 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC Листинг 31 .2 . Содержимое файла Startup. cs usi n g Microsoft . AspNetCore . Bu ilde r ; using Microsoft . Extensions . Dependencyinjection ; n ames p ace Conventio n sAndCo n straint s puЬli c c l ass Startup { puЫic void ConfigureServices(IServi ceCol l ect i on s e rvices) { services.AddМvc(); puЫic void Configure(IAppl icat ion Bui l d e r арр) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app . UseStaticFiles(); app.UseMvcWithDefaultRoute(); Создани е модели представл ен и я , контроллера и предста вле ния Во многих примерах главы полезно знать, какой метод использовался для обра­ ботки запроса. С этой целью со здайте папку Model s и добавьте в нее файл кл асса по имени Resul t . cs с определ ением, показанным в листинге 31.3 . Данный класс поз­ волит контролерам в настоящей главе пер едавать представлению информацию о том , как был обработан запрос. Листинг 31 .З. Содержимое файла Resul t. cs из папки Models usi ng System . Collections . Gener i c ; namespace Convent i onsAndConstr aint s.Models puЫic cl ass Resul t { puЫic string Contro l ler { get ; set ; puЫic st ring Action { get; set; } В этой главе требуется еди нственный контроллер и представление. Со здайте пап­ ку Controllers и добавьте в нее файл класса по имени HomeCo n trolle r . cs с опр е ­ делением из листинга 31.4 . Листинг 31 .4 . Содержимое файла HomeController. cs из папки Controllers u sing ConventionsAndConstra ints . Models ; usi n g Microsoft . As pNetCore . Mvc ; n amespace Convent i onsAndConstra i n t s . Controllers puЫic cl ass HomeController : Controll er { puЫ i c IActionResul t I ndex() => Vi ew( "Result", new Result { Cont r ol ler = nameof(HomeContr oll er ) , Actio n = nameof( I ndex) });
Глава 31. Соглашения по модели и ограничения действий 961 puЫic IActionResult List() => View( "Re sult", new Result { Controller = nameof(HomeController), Action = nameof(Lis t) }); Оба метода действий в контроллере Home визуализируют представление Result. Создайте папку Views/Home и поместите в нее файл Resul t . csh tml с разметкой, приведенной в листинге 31.5 . Листинг 31 .5 . Содержимое файла Resul t. cshtml из папки Views/Home @model Resu l t @{ Layout = null ; <!DOCTYPE html> <html> <head> <meta name="viewport " content="width=device-width" /> <l ink asp -href - include= " liЬ/boo ts tr ap /dis t /css/* . min . css " rel= " stylesheet " /> <title>Result</title> </head> <body cl ass= " panel-body"> <tаЫе c la ss= "t aЫe taЫe-condensed t a Ы e -b ordered " > <tr><th>Controller : </th>< td>@Model .Control l er</ t d></tr> <tr><th>Action : </th><td>@Model . Action</td></tr> </tаЫе> </body> </html> При стилизации НТМL-элементов представление полагается на СSS-пакет Bootstrap. Создайт е в корневой папке проекта файл bower . j son с использованием шаблона элемента Bower Configuration File (Файл конфигурации Bower) и добавьте па­ кет Bootstrap в раздел dependencies (листинг 31.6). Листинг 31 .6. Добавление пакета Bootstrap в файле bower. j son "name ": "asp . net", " private ": true , " dependencies ": { "bootstrap": "3.3.6" Финальный подготовительный шаг связан с созданием внутри папки Views файла _ Viewimports. cshtml . в котором настраиваются встроенные дес1<рипторные вспо­ могательные юшссы для применения в представлениях, а также импортируется про­ странство имен модели (листинг 31. 7).
962 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Листинг 31. 7. Содержимое файла_Viewimports. cshtml из папки Views @using Convent i onsAndConstraints .Model s @addTagHelper * , Mi crosof t .AspNetCor e .Mvc . TagHelpers Запустив приложение, вы увидите вывод, показанный на рис. 31.1 . Controller: HomeController Action: lndex Рис. 31 .1 . Запуск примера приложения Использование модели приложения и соглашений по модели В MVC поддерживается соглашение по конфигурации, благодаря чему можно просто создать класс с именем, заканчивающимся на Co n troller, и приступить к определению методов действий. Во время выполнения инфраструктура МVС приме­ няет процесс обнаружения для нахождения всех контроллеров и действий в прило ­ жении и инспектирует их, чтобы выяснить, используют ли они средства , подобные фильтрам. Итоговым результатом процесса обнаружения является модель приложения, состо­ ящая из объектов, которые описывают каждый найденный класс контроллера, метод действия и параметр. Соглашения, на которые опирается MVC, применяются к моде­ ли приложения по мере ее построения . Например, когда обнаруживается класс конт­ роллера, имя этого класса используется в качестве основы для контроллера, который представляет его в модели ; другими словами, класс HomeCo n tro l le r применяется для создания контролл е ра Н оте . Когда система маршрутизации идентифицирует за­ прос, подлежащий обработке контроллером Home, именно модель приложения предо­ ставляет отображение на ~тасс HomeCon trol l er. Модель приложения можно настраивать с использованием соглашений по модели . Соглашения по модели - это классы, которые инспектируют содержимое модели при­ ложения и вносят корректировки, такие как синтез новых действий либо изменение способа применения классов для создания контроллеров . В последующих разделах объясняется, как структурирована модель приложения, рассматриваются различные типы соглашений по модели и демонстрируются приемы испо льзования этих согла­ шений. В табл. 31 .2 приведена сводка, позволяющая поместить модель приложения и соглашения по модели в контекст.
Глава 31. Соглашения по модели и ограничения действий 963 Таблица 31 .2. Помещение модели приложения и соглашений по модели в контекст В опрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие­ то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравне­ нию с версией MVC 5? Модель приложения Ответ Модель приложения представляет собой полное описание контроллеров и действий, которые были обнаружены в приложении.Соглашения по модели позволяют применять к модели приложения специальные изменения Соглашения по модели удобны, потому что они делают возможным внесение изменений в способ отображения классов и методов на контроллеры и действия. Могут вы­ полняться и другие настройки, такие как ограничение НТТР­ методов, которые принимаются действиями, или примене­ ние ограничений действий (рассматриваются далее в главе) Соглашения по модели определяются с использованием набора интерфейсов, описанных в последующих разделах, и применяются посредством атрибутом или конфигуриру­ ются в классе Startup Со способом применения соглашений по модели связаны определенные странности, которые обсуждаются в после­ дующих разделах Нет, хотя можно ввести собственные компоненты для со­ здания специальной модели приложения , если стандарт­ ная модель не соответствует имеющимся потребностям Модель приложения - это новое добавление к инфраструк­ туре MVC Во время процесса обнаружения инфраструктура МVС создает экземпляр класса ApplicationModel и заполняет его сведениями о найденных контроллерах и дейс­ твиях. Посл е завершения процесса обнаружения применяются соглашения по модели, чтобы внести все специальные изменения, которые были указаны. Отправной точ1<ой для поним ания модели приложения является исследование свойств, определяемых клaccoм Microsoft . AspNetCore . Mvc . ApplicationModels . ApplicationModel, которые описаны в табл. 31.3 . На заметку! Может показаться, что мы начинаем с очень простого места, особенно если вы стремитесь немедленно погрузиться в детали, но полезно оценить, насколько полно классы, рассматриваемые в этом разделе, описывают основные части приложения MVC. Понимание работы модели приложения поможет понять внутреннее функционирование более сложных средств, что способствует лучшей подготовке к диагностированию про­ блем, когда проекты выдают непредвиденные результаты .
964 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 31 .3. Избранные свойства класса ApplicationМodel Имя Описание Controllers Это свойство возвращает объект реализации IList<ControllerModel> , который содержит все контроллеры в приложении Filters Это свойство возвращает объект реализации I List< I Fi 1 terMetada ta>, который содержит глобальные фильтры в приложении В настоящей главе интерес представляет свойство Cont r o ll ers , возвращающее список, который содержит объекты Co n t r ol l e r Model для каждого контроллера, об­ наруженного в приложении . Наиболее важные свойства класса ControllerModel описаны в табл . 31.4 . Таблица 31 .4. Избранные свойства класса ControllerModel Имя ControllerName ControllerType Control l erPr operties Action s Fi lters RouteCon str a i nts Sel ectors Описание Это свойство типа string определяет имя контроллера. Данное имя будет использоваться при сопоставлении с сегментом маршрутизации controller Это свойство типа Typeinfo определяет тип класса контроллера Это свойство возвращает объект реализации IL ist<Prope r tyMo d e l > , который описывает все свойства , определяемые контроллером (табл . 31.5) Это свойство возвращает объект реализации ILi st<Acti onModel>, который описывает все действия , оп­ ределяемые контроллером (табл . 31 .6) Это свойство возвращает объект реализации IList<IFilterMetadata>, который содержит фильтры, применяемые ко всем действиям в контроллере Это свойство возвращает объект реализации ILi s t < I Rout eCon s traintPr ovider> , который использу­ ется для ограничения нацеливания маршрутов на действия, определяемые контроллером Это свойство возвращает объе кт реализации I List<Sel ect o r Model>, который содержит детали ограни­ чений действий (описанные в разделе " Использование ограни­ чений действий " далее в главе) и информацию маршрутизации , применяемую к контроллеру посредством атрибутов , как было показано в главе 15 Как видите, классы модели приложения охватьmают часть основной фун1{цион аль­ ности MVC . Свойство ControllerNa me, наприм е р, используется для установки им е ­ ни , которое будет задействовано системой маршрутизации при сопоставлении URL, а свойство ControllerType применяется для установки класса контроллера , к кото­ рому это имя относится.
Глава 31 . Соглашения по модели и ограничения действий 965 Свойство ControllerProperties возвращает список объектов PropertyModel, каждый из которых описывает свойство, определяемое контроллером. Самые важные свойства класса PropertyModel перечислены в табл. 31.5 . Таблица 31 .5 . Избранные свойства класса PropertyModel Имя PropertyNarne Attributes Описание Это свойство типа string возвращает имя свойства Это свойство возвращает список атрибутов, которые были применены к свойству Свойство Actions возвращает список объектов ActionModel, каждое из которых описывает метод действия, определяемый одиночным классом контроллера. Наиболее важные свойства класса ActionModel перечислены в табл. 31 .6. Таблица 31 .6. Избранные свойства класса ActionМodel Имя ActionName ActionMethod Controller Filters Parameters RouteConstraints Selectors Описание Это свойство типа string определяет имя действия, которое будет использоваться при сопоставлении с сегментом маршрути­ зации action Это свойство типа Methodinfo применяется для указания мето­ да, который реализует действие Это свойство возвращает объект ControllerModel , описываю­ щий контроллер, которому принадлежит действие Это свойство возвращает объект реализации IList<IFil terMetadata>, содержащий фильтры, которые при­ меняются к действию Это свойство возвращает объект реализации IList<PropertyModel>, который содержит описания парамет­ ров, требуемых методом действия Это свойство возвращает объект реализации IList<IRouteConstraintProvider>, который используется для ограничения нацеливания маршрутов на действие Это свойство возвращает объект реализации IList<SelectorModel>, который содержит детали ограничений действий (описанные в разделе "Использование ограничений дейс­ твий" далее в главе) и информацию маршрутизации, применяемую к контроллеру посредством атрибутов , как объяснялось в главе 15 Последний уровень детализации доступен через свойство Parameters, возвраща­ ющее список объектов ParameterModel, которые описывают каждый параметр, оп­ редел яемый методом действия. Наиболее важные свойства класса ParameterModel перечислены в табл. 31.7 .
966 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Таблица 31.7. Избранные свойства класса ParameterModel Имя ParameterName Parameterin fo Bindinginfo Описание Это свойство типа string используется для указания имени параметра Это свойство типа Proper tyinfo применяется для указания параметра Это свойство типа Bin d i n g i n f o используется для конфигурирова­ ния процесса привязки моделей, как было показано в главе 27 Типы App licationMo d el, Co ntrollerMode l, P ropert yMo d el , Ac t ionModel и Par a me t erModel применяются для описания любого аспекта классов I{ОНтроллеров в прилож ении, а тю<же их методов, свойств и параметров , опр еде ля емых к аждым методом . Настройка модели приложения Инфраструктур а MVC имеет несколько встроенных соглаш ений, которые она применяет в ходе заполнения Applic atio nM odel объектами Controll erModel, PropertyMode l , ActionMode l и Pa r ame t e r Model с целью описания обнаруживае­ мых контроллеров . Одни соглашения являются явными, такие как удаление строки Controller и з имени 1macca контроллера и затем использование р езультата для уст ановки свойства Control le r Name объекта Con t r o lle rM odel. Это соглашение позволяет определять класс вроде Ho meController, но нацеливаться на него с помощью сегм е нта URL , ко­ торый содержит Home. Другие соглашения являются неявными, например, прим енени е каждого класса для создания одного контроллера и каждого метода для создания одного действия. Большинство разработчиков приложений MVC воспринимают эти с огл аш е ния как не ­ что само собой разумеющееся и совершенно не задумываются о них. однако каждый аспект модели приложения может быть изменен. В пр едшествующих главах были описаны атрибуты. которы е и з меняют способ работы инфраструктуры MVC. и в действительности это соглаш е ния по модели. Упомянутые атрибуты перечислены в табл. 31.8 . Таблица 31 .8. Базовые атрибуты, которые изменяют стандартные соглашения приложения Имя Act i onName No n Control l er NonAction Описание Этот атрибут позволяет явно указывать значение для свойства Act i onName объекта Act i onModel вместо того , чтобы выводить его из имени метода Этот атрибут предотвращает применение класса для создания объекта Co n trollerMode l Этот атрибут предотвращает использование метода для создания объекта Act i onМode l В листинге 31 .8 атрибут Actio nNam e применяется для измен е ния им е ни дейс­ твия, которое создано для представления метода L i s t () в класс е HomeController .
Глава 31 . Соглашения по модели и ограничения действий 967 Листинг 31 .8. Настройка модели приложения в файле HomeController. cs using Conventions AndConstra i nts.Models; using Mi crosoft . AspNe t Co re. Mvc ; n ame space Conve nti o nsAndConstraint s.Cont r o l l ers puЫic class HomeController : Control l er { puЫic IActionResult Index() => Vi ew ("Result ", new Res ult { Controller = nameof(HomeCo ntr ol l er) , Action = nameo f (Inde x ) }); [ActionName ( "Details") ] puЬlic IActionResult List () => View("Result", new Result { Controlle r = nameof(HomeCont rol ler ) , Act i on = nameof( List) }); Здесь указано , что для создания действия должно использоваться имя Det ail s , заменяя собой стандартное имя List. Чтобы взглянуть на результат, запустите при­ ложение и запросите URL вида /Home/Detai l s. Как показано на рис . 31.2, запрос обрабатывае тся методом Li s t () . .. ::> С [] localhost:65325/Home/Details - = Controller: HomeController Action: List Рис. 31 .2. Настройка модели приложения Роль соглашений по модели Атрибуты, описанные в табл. 31.8, дают возможность вносить базовые измене­ ния в модель приложения , но ограничены в своей области действия. Для более су­ щественных настроек требуются соглашения по модели (также известные как просто соглашения) . Атрибуты из табл. 31 .8 разрешают указывать изменения в объектах модели прило­ жения до их создания , например, переопределение имени, назначаемого действию. По контрасту с этим создание соглаш ения по модели позволяет изменять модель прило­ жения, модифицируя объекты модели после их создания, что открывает возможности для применения бол ее обширных изменений . Доступны ч етыре вида соглашений по модели, каждый из которых определяется своим интерфейсом (табл. 31.9) .
968 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Соге MVC Таблица 31 .9 . Интерфейсы соглашений по модели приложения Имя Описание IApplicationModelConvention Этот интерфейс используется для применения согла­ шения к объекту App l icationМode l I ControllerModelConven tion Этот интерфейс используется для применения соглаше­ ния к объектам ControllerModel в модели приложения IActionModelConvention Этот интерфейс используется для применения согла­ шения к объектам ActionМode l в модели прило жения IParameterModelConvention Этот интерфейс используется для применения соглаше­ ния к объектам ParameterModel в модели приложения Все четыр е интерф ейса работ ают аналогично : меняется только уровень, на кото­ ром они функционируют внутри модели представлен ия . Напри мер. вот определе ние интe pфeйcaIControllerModelConvention : namespace Microsoft . AspNetCore . Mvc . ApplicationModels puЫic interface I Controlle r Mode l Co nvention void App l y(ControllerModel control ler) ; С помощью вызов а метода App l y ( ) соглашению по модели предоставля ется во з ­ можно сть внести изменения в объект Cont r o llerMode l , к которому оно был о при­ менено и который передается методу в качестве аргумента. Другие интерф ейсы также определяют методы Арр l у ( ) , причем кажды й и з них получ ает объект моде ­ ли модифицируемого им типа, так что интерфейс IActionModelConvention при­ ни мает объект ActionModel, а интерфейс I ParameterModelConvention - объ ект ParameterModel. Создание соглашения по модели Соглаш е ния по модели для контроллеров , действий и парам етров могут п рим е ­ няться как атрибуты, что облегчает установку обла сти действия вносимых ими из ­ мен ений. Создайте папку Infrastr u c tu re и добавьте в нее файл класс а по им е ни ActionNamePrefixAttribute . cs , содержимо е которого приведено в лис тинг е 31.9. Листинг 31 .9. Содержимое файла ActionNamePrefixAttribute. cs из папки Infrastructure using System ; using Microsoft . Asp NetCore.Mvc . Appl i ca t ionMode l s ; namespace ConventionsAndCon straints .Infrastructure [AttributeUsage (AttributeTargets . Me t hod , Al l owM u ltiple = false) ] puЫic c l ass ActionName PrefixAttribute : Attribute , I ActionMode l Convention private str i ng namePrefix ; puЫic ActionNamePrefixAttribute (stri ng prefix) { namePrefix = prefix ;
Глава 31 . Соглашения по модели и ограничения действий 969 puЬlic void Ap ply(ActionMode l acti on) { act ion.Act ionName = namePrefix + action.ActionName; Класс Ac tionNamePre fi xAt tr i bute унаследован от класса Attribu te и реали­ зует инт ерфейс I ActionM o delConven ti o n. Его конструктор принимает строку, ис­ пользуемую в кач естве префикса, который применяется путем модификации свойства ActionName объ екта Acti onModel . полученного методом Apply () . Совет. Обратите внимание, что использование атрибута Act i onNamePrefix ограничива ­ ется так, что он может быть применен только к методам. Когда соглашения по модели применяются как атрибуты, соглашения по контроллерам оказывают воздействие только в случае их применения к классам, соглашения по действиям - только в случае приме­ нения к методам, а соглашения по параметрам - только в случае применения к парамет­ рам . Соглашение , примененное на неправильном уровне , будет просто проигнорировано без возникновения ошибки. Во избежание путаницы используйте Att ribu t eUsage , что­ бы ограничить область действия создаваемых атрибутов . В листинге 31. l О атрибут соглашения по модели применяется к одному из методов действий контроллера Horne . Листинг 31 .1 О. Применение соглашения по модели в файле HomeController. cs using Conv e ntionsAndCon st raints . Mod e ls ; using Microsof t. As pNe tCo re.Mvc ; using ConventionsAndConstraints.Infrastructure; namespace ConventionsAn dC o n stra i n ts. Con trollers puЫic class HomeCon t ro l ler : Control l er { puЬlic IActionResult Index() => View("Result 11 , new Result { Control ler = nameof (HomeController) , Action = nameof (I nd ex) }); [ActionNamePrefix ( 11 Do 11 ) ] puЫic IActi onResul t List () => View ("Result", new Resul t { Control l er = nameof (HomeCo ntr o lle r ) , Action = nameof(List ) }); Во время прохождения процесса обнаружения инфраструктура МVС создаст объ­ ект Action Model, описывающий метод List () , выявит Ac t ion NameP r e fi x и вызо­ вет его метод Apply () . Чтобы посмотреть на эфф ект, запустите приложение и запро­ сите URL вида / Horne/ Do List, заменяющий URL, который был бы нацелен на метод Li st () при стандартных соглашениях (рис. 31 .3).
970 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Саге MVC Controller: HomeController Action: List Рис. 31 .З. Применение соглашения по модели Использование соглашений, добавляющих или удаляющих объекты модели Со способом применения соглашений по модели связана одна индивидуальная особенность, которая предотвращает добавление или удаление ими объектов модели . Например, предположим, что нужно создать соглашение, которое определенные мето­ ды смогли бы достигать через два разных действия. Добавьте в папку Infrastructure файл класса по имени AddActionAttribute . cs с содержимым из листинга 31.11. Листинг 31, 11. Содержимое файла Ad.dActionAttribute. cs из папки Infrastructure using System; using Microsoft . AspNetCore . Mvc.ApplicationModels ; namespace ConventionsAndConstraints.Infrastructure [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] puЫic class AddActionAttribute : Attribute, IActionModelConvention private string additionalName ; puЫic AddActionAttribute(string name) { additionalNarne = narne; puЬlic void Apply(ActionModel action) { action . Controller . Actions.Add(new ActionModel(action) ActionNarne = additionalNarne }); Это соглашение по действиям использует конструктор ActionModel. который дуб­ лирует настройки существующего объекта и затем изменяет свойство ActionName нового экземпляра. Новый объект ActionModel добавляется в коллекцию действий контроллера за счет навигации с помощью свойства ActionModel. Controller . В листинге 31. 12 демонстрируется применение данного соглашения по модели к конт­ ролл еру Home.
Глава 31 . Соглашения по модели и ограничения действий 971 Листинг 31 .12. Применение соглашения по модели в файле HomeController. cs using ConventionsAndConstraints.Models ; using Microsoft .AspNe t Core .Mvc; using ConventionsAndConstraints .I nfrastructure ; namespace ConventionsAndConstraints . Controllers puЫic class HomeController : Controller { puЬlic IActionResult Index() => View("Result", new Result { Control l er = nameof(HomeController), Action = nameof(Index) }); [AddAction("Details")] puЬlic IActionResult List() => View( "Result ", new Result [ Controller = nameof(HomeController), Action = nameof(List) }); После запуска приложения инфраструктура МVС начнет процесс обнаружения и сообщит о следующей ошибке: InvalidOperationException : Collection was modified; enumeration operation may not execute . InvalidOperationException : коллекция была модифицирована; операция перечисления может не выполниться. Соглашение по модели пытается изменить набор объектов действий модели по ме ре его перечисления процессом обнаружения, что приводит к исключению . Чтобы избежать ошибки, потребуется избрать другой подход (листинг 31.13). Листинг З 1.13. Создание безопасного соглашения по модели в файле Add.ActionAttribute.cs using System ; using Microsoft.AspNetCore.Mvc.ApplicationModels; using System.Linq; namespace ConventionsAndConstraints . Infrastructure [AttributeUsage(AttributeTargets.Method, AllowMultiple puЬlic class AddActionAttribute : Attribute { puЫic string AdditionalName { get; } puЫic AddActionAttribute(string name) AdditionalName = name; true)] [AttributeUsage(AttributeTargets . Class, AllowMultiple puЫic class AdditionalActionsAttribute : Attribute, IControllerModelConvention { false)] puЫic void Apply(ControllerModel controller) var actions = controller.Actions
972 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC . Select(а => new { Action =а, Names = a.Attributes.Select(attr => (attr as AddActionAttribute)?.AdditionalName) }); foreach (var item in actions.ToList()) { foreach (string name in item.Names) { controller.Actions.Add(new ActionМodel(item.Action) ActionName = name }); Модифицировать набор действий. ассоциированных с контроллером. внутри со­ глашения по действиям невозможно, но все равно необходим какой-нибудь способ обозначения требующихся изменений. По этой причине класс AddActionAttribute был сделан просто атрибутом, а не соглашением по модели. Изменять набор действий внутри соглашения по контроллерам допускается, поэ­ тому был создан класс AdditionalActionsAt tribute. Метод Apply () использует LINQ для нахождения методов, к которым был применен класс AddActionAttribute . и создает новы е объекты Ac t ionModel с указанными именами. Самой важной частью данного класса является вызов метода ToList (), приме­ ненный к результатам LINQ: foreach (var item i n actions .ToList ()) Метод ToList () инициирует перечисление запроса LINQ и помещает результат в новую коллекцию, а это значит, что цикл foreach проходит по другому набору объек­ тов, который отличается от набора, перечисляемого инфраструктурой МVС во время применения соглашений по модели. Без вызова ToL i st () было бы получено то же самое сообщение об ошибке, которое сгенерировало соглашение по модели из лис­ тинга 31 .12; благодаря вызову To List () появляется возможность создавать новые объ екты действий в модели. В листинге 31.14 иллюстрируется применение переде­ ланных атрибутов к контроллеру Home. Листинг 31.14 . Применение переделанного соглашения по модели в файле HomeController.cs using ConventionsAndConstraints . Mod els; using Microsoft.AspNetCore . Mvc ; using Convent i onsAndConstra in ts . I nfrast ru cture ; namesp ace ConventionsAndConstraints . Contro ll ers [AdditionalActions] puЫic class HomeController : Co ntro ller { puЫic IActionResul t Index () => View ( " Resul t ", new Resul t ( Contro l ler = nameof(HomeCo n troller) , Action = nameof(Index) });
Глава 31 . Соглашения по модели и ограничения действий 973 [AddAction ( " Details " ) ] puЬlic IActionResul t List () => View ("Result ", new Result { Controller = nameof( HomeCo nt r oll er) , Action = nameof( List) }); Для просмотра эффекта от переделанного соглашения по модели запустите при­ ложение и запросите URL вида /Home/Deta il s и / Hom e / Lis t . Соглашение по мо­ дели добавл яет новое действие, которое обрабатывается методом Li st () ,дополняя со зданную по умолчанию модель (рис . 31.4). 1.R•sult х /+- - t С : D localhost:65325/Home/List 1--::0~:;--- HomeController ~-~~~~L-is-t~- Controller: HomeController Action: List Рис. 31 .4. Результат создания действия в модели П орs1Док выполнен и я соглашений по модели Соглашения по модели применяются в специфическом порядке, начиная с со­ глашений, имеющих наиболее широкую область действия: первыми применяются соглашения по контроллерам , затем соглашения по действиям и, наконец, согла­ ш е ния по парам ет рам. Для демонстрации такого порядка к методу List () класса HomeControl l er применяются оба специальных соглашения , созданные в предшес­ твующих при мерах (листинг 31.15). Листинг З 1.15. Применение нескольких соглашений по модели в файле HomeController.cs using ConventionsAndCons traints . Mo del s ; using Microsoft . AspNetCore.Mvc ; using ConventionsAndCon s traints .Infra structure ; namespace Co nventionsAndCo n straints. Co ntrolle rs [AdditionalActions ] puЫic class HomeController : Con t ro ller { puЫic IActionRes u lt Index() => Vi ew( " Result ", new Result { Cont r oller = nameof(HomeControl l er) , Action = nameof(Ind ex) }); [ActionNamePrefix ( "Do")] [AddAct i on( " Details " ) ]
974 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC puЫic IActionResul t List () => View ( "Resul t ", new Resul t Controller = nameof(HomeController), Action = nameof(List) }); Атрибут Addi tionalActions, который является соглашением по контроллерам, применяется первым и создает новое действие по имени Details . Затем применяет­ ся атрибут ActionNamePrefix , представляющий собой соглашение по действиям, ко­ торый применяет префикс Do ко всем действиям, ассоциированным с методом. В ре­ зультате метод List () реализует два действия, DoList и DoDetails, которых можно достичь через URL вида /Home/DoList и /Home/DoDetails (рис. 31.5). 1. Result х Action : List ------~-...:.:___ . ~:.....- ____ __________.....,. ___ ,..____ _ ~-:_:_,~_:r_:_:ll-e-r:_____~_i:_~_e_C_o_n_tr-o-lle_r___ ___J Рис. 31 .5 . Иллюстрация порядка выполнения соглашений по модели Создание глобальных соглашений по модели Может возникнуть необходимость изменить стандартные соглашения по модели для каждого контроллера, действия или параметра в приложении. В таком случае вместо того, чтобы обеспечить согласованное применение атрибутов к каждому классу контроллера, можно создать глобальное соглашение по модели. глобальные соглаше­ ния по модели конфигурируются в классе Startup, как показано в листинге 31.16. Листинг 31 .16. Соэдание глобальных соглашений по модели в файле Startup. cs using Microsoft .AspNetCore.Builder ; using Microsoft .Ext ensions.Dependencyinjection; using ConventionsAndConstraints .I nfrastructure; namespace ConventionsAndConstraints puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc() .AddМvcOptions(options => { options.Conventions.Add(new ActionNamePrefixAttribute("Do")); options.Conventions.Add(new AdditionalActionsAttribute()); });
Глава 31 . Соглашения по модели и ограничения действий 975 puЫic void Configure(IApplicationBuilder арр) { app . UseStatusCodePages() ; app.UseDeveloperExceptionPage() ; app.UseStaticFiles() ; app . UseMvcWithDefaultRoute() ; Объект MvcOpt i ons, получаемый расширяющим методом AddMvcOptions () , определяет свойство Conventions. Это свойство возвращает коллекцию, в которую можно добавлять объекты соглашений по модели. В листинге 31 . 16 оба специаль­ ных соглашения по модели применены глобально, т.е. имена всех действий будут снабжены префиксом Do, а все методы будут инспектироваться на предмет атрибута AddAction . Поскольку соглашения по модели применяются глобально, атрибуты из класса HorneController можно удалить (листинг 31.17). Листинг 31 .17 . Удаление соглашений по модели в файле HorneController. cs using ConventionsAndConstraints . Models ; using Microsoft . AspNetCore . Mvc ; using ConventionsAndConstraints . Infrastructure ; namespace ConventionsAndConstraints . Controllers // [AdditionalActions] puЬlic class HomeController : Controller { puЫic IActionResult Index() => View( " Result ", new Result { Controller = nameof(HomeControlle r ) , Action = nameof(Index) }); / / [ActionNamePrefix ( "Do")] [AddAction( " Details ") ] puЫic IActionResult List () => View ( "Result", new Result { Controller = nameof(HomeController) , Action = nameof(List) )); IЛобальны е соглашения по модели применяются перед соглашениями, применен­ ными напрямую к классам. Если глобальных соглашений несколько, то они приме­ няются в порядке , в котором регистрировались, безотносительно их типов. В лис­ тинге 31.16 соглашение по действиям было зарегистрировано перед соглашением по контроллерам, а это означает, что действие Deta ils, указанное посредством атрибу­ та AddAction, создается после того, как соглашение ActionNamePrefixAttribu te будет применено ко всем именам действий. В результате метод List () реализует два действия , DoList и Details , которые достижимы через URL вида /Horne/DoList и /Home/Details (рис. 31.6).
976 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC 1 Result Х ~~~· С : CJ localh~~~.:6S3~5/.H~~ef_?~a~~~~ c{J_~ =-.! Controller: HomeCon troller 1 / Action: List 1 Рис. 31 .6. Иллюстрация порядка выполнения глобальных соглашений по модели Использование ограничений действий Ограничения действий решают, подходит ли метод действия для обработки специ­ фического запроса, и это может навести на мысль, что ограничения действий подоб­ ны фильтрам авторизации, которые рассматривались в главе 19. На самом деле применение ограничений действий намного уже. Когда инфраструк­ тура МVС получает НТГР-запрос , она проходит через процесс выбора с целью иден­ тификации метода действия, который будет использоваться для обработки запроса. При наличии нескольких методов действий, которые могли бы обработать запрос, ин­ фраструктуре MVC нужен какой-нибудь способ решения, какой из них применить, и именно здесь используются ограничения действий. В табл. 31.10 приведена сводка, позволяющая поместить ограничения действий в контекст. Таблица 31 .10. Помещение ограничений действий в контекст Вопрос Что это такое? Чем они полезны? Как они используются? Существуют ли какие-то скрытые ловушки или ограничения? Существуют ли альтернативы? Изменились ли они по сравнению с версией MVC 5? Ответ Ограничения действий - это классы, которые инфраструктура MVC применяет для выяснения, может ли запрос быть обрабо­ тан специфическим действием Если обработать запрос способны два или большее число дейс­ твий, то MVC необходимы средства для решения, какое дейс­ твие подходит лучше других. Ограничения действий использу­ ются для предоставления такой информации Ограничения действий применяются как атрибуты, что позволяет их многократно использовать повсюду в приложении, а также оз­ начает, что логика определения , должно ли действие обработать запрос, не обязана находиться внутри самого метода действия Ограничения действий могут быть применены слишком широко и препятствовать обработке запроса любым подходящим мето­ дом действия, приводя к тому, что клиенту отправляется ответ 4О 4 - Not Found (404 - не найдено) Фильтры более удобны, если нужно ограничить доступ к дейс­ твиям в специфических обстоятельствах, т.к. клиента можно пе­ ренаправить для отображения полезной страницы ошибки Ограничения действий теперь являются неотъемлемой частью модели приложения, которая отсутствовала в предшествующих версиях MVC
Глава 31. Соглашения по модели и ограничения действий 977 Подготовка проекта для примера Цель ограничений действий - помочь инфраструктур е МVС сделать выбор среди двух и бол ее похожих методов, когда любой из них мог бы использ оваться для обра­ ботки запроса. Такая ситуация создается в листинге 31. 18 за сч ет добавления нового метода действия в контроллер Ho me . Листинг З 1.18. Создание двух одинаково подходящих действий в файле HomeController. cs using Co n ventionsAn dConstra i nts . Mo d e ls; usi n g Mi crosoft.AspNetCore . Mvc ; using ConventionsAndConstra int s .Inf rastr u ct u re ; namespace Conventi onsAndConst r aints.Cont r oll e r s 11 [Addi t i onal Actions ] puЫic class HomeControll er : Co ntroll er { puЫic IActionResult Inde x( ) => View( " Result ", new Result { Controller = nameof( Ho meContro ller) , Action = na meof(Index) }); [ActionName("Index")] puЫic IActionResult Other() => View("Result", new Result { Controller = nameof(HomeController), Action = nameof(Other) }); 11 [Act i onNamePrefix( "D o " ) ] [AddAction ( " Details " )] puЫic IActionResult List () => View("Result", new Result { Control ler = nameof(Home Co n t r o ller) , Action = nameof(List) }); Здесь бьш добавл ен новый метод по имени Oth er ( ) , к которому применен атрибут ActionName, так что он выпускает действие под названием I ndex . Кро м е того, был обновлен класс Startup для удаления глобальных соглашений по модели, прим е нен­ ных ранее в главе (листинг 31.19). Листинг 31.19. Удаление глобальных соглашений по модели в файле Startup. cs puЫic void Confi gu r eServices(IServi ceCo l lection services) { services . AddMvc() . AddMvcOpt i ons(options => { 11 options . Conventions.Add(new ActionNamePrefixAttribute("Do")); 11 options.Conventions.Add(new AdditionalActionsAttribute()); });
978 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC Таким образом, в контролл е ре Ноте есть два действия по имени Index. Запустив приложение , вы увидите сообщение об ошибке, показанное на рис. 31.7, которо е сви­ детельствует о том, что МVС не известно, какое действие должно использоваться. J lnte mol Servor Error Х +- ~ С Dlocall10st:65325 ---------------'---' An unhandled exceptio п оссштеd while processing the request. ~ AmЬiguousActionE xcept юn : lviultiple actions matched . Th e fo !l o\'Jiп g actions matc hed rou е cJata апd hacl all coпstraints satisfied: Const1·a intsAndCoпvent ion s.Con·юllers.HomeCo ntro l lecl ndex Co nstraintsA ndC oпveпtions.Contгolleгs.HomeC ontюller. Otl1 eг Mic rosoft.AspNet .M vc.1 nfras t ruc t ure.De fau ltActionSele ctcr.SelectAsync(Rou t eCon t ext context) Рис. З 1. 7 . Результат создания двух одинаково подходящих методов действий Для удобства ниже приведена важная часть сообщения: AmbiguousActionExcept i on : Mul tiple a ctions matched . The foll owing actions matched route data and had a ll cons traints sati sfied : ConventionsAndConstrai nts.Cont r ollers.HomeCont r oll er .Index Con venti onsAndCons t rai nt s . Controllers. HomeControl l e r . Other AmbiguousActionEx cep t ion : соот ве т с тв ие нескольким действиям . Следующи е дейс твия со отв ет ств ую т д а нным маршрута и все их ограничения удовле тв орены : Conve n t ionsAn d Constraint s. Cont r ollers . Ho meContro ll er .Index Convent ionsAndConstrai nt s . Con trol lers.HomeControl ler. Other Огран и чения действий Огран ичения действий применяются для указания инфраструктуре МVС , может ли метод действия использоваться для обработки запроса, и для реализации интер­ фейса I Ac tio n Co ns tra i nt, который определен следующим образом: namespace Mi crosoft.AspNetCor e .Mvc .ActionConst r aints { puЫic interface IActionConstrai nt : IActionConstr ai ntMetadata int Order {get; } bool Accept (Acti onConstrai nt Context context) ; Когда инфраструктура МVС проходит через процесс выбора метода действ ия для обработки запроса, она выясняет, связаны ли с ним ограничения. При нали ч ии ог­ раничений они упорядочиваются в последовательность на основе значения свойства Or de r, и по очереди для каждого ограничения вызывается метод Accept () .Если дл я любого ограничения метод Accept () возвращает f a lse, тогда инфраструктура МVС считает, что метод действия не может применяться для обработки текущего запроса.
Глава31. Соглашения no модели и ограничения действий 979 Со вет. Интерфейс IAct i onConstraint является производным от IActionConstraint Metadata, который представляет собой интерфейс , не определяющий членов . Он не используется напрямую, и при определении специальных ограничений вы всег­ да дол ж ны применять интерфейс IActionConstaint , а при создании ограничения, к оторое имеет зависимости , подле ж ащие распознаванию, использовать интерфейс IActionConstra i ntFactory, как описано в разделе "Распознавание зависимостей в ограничения х де й ствий" далее в главе. Чтобы помочь ограничениям действий принять решение , инфраструктура MVC снабжает их экземпляро м клас са Act i onConstraintContext для данных конт екс­ та, который определяет свойства, описанные в табл. З 1.11. Таблиц а 31 . 11. Свойств а класса ActionConstraintContex t Имя Candidates CurrentCandidate RouteContext Описание Это свойство возвращает список объектов ActionSelectorCandidate, описывающий набор методов действий, которые инфраструктура MVC включила в окончатель­ ный сп и со к для обработки текущего запроса Это свойство возвращает объект ActionSel ectorCandidate, описывающий метод действия, оценка которого затребована у ограничения Это свойство возвращает объе кт RouteContext, который пре­ доставляет информацию о данны х маршрутизации (чер ез свойс­ тво RouteData ) и НТТР-запросе (через свойство HttpContext ) Создание ограничения действ и я Самый распространенный тип ограничения исследует запрос, чтобы удостове­ риться в удовлетворении некоторой политики, такой как наличие определенного значения в заголовке НТГР. Чтобы посмотреть, каким образом создается данный вид ограничения действия, добавьте в папку Infrastructure файл класса по имени UserAgentAttribute . cs и поместите в него содержимо е и з листинга 31.20. Листи нг 31.20. Содержимое файла UserAgentAttribute.cs из папки Infras tructure using System ; using System.Linq ; using Microsoft.AspNetCore.Mvc . ActionConstraints ; namespace ConventionsAndConstraints .I nfrast r ucture puЫic class UserAgentAttribute : Attribute , IActionConstraint private string substring ; puЬlic UserAgentAttribute(string sub) { substring = sub . ToLower() ; puЬlic int Order { get ; set ; О;
980 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC puЫ i c bool Accept(ActionConstraintContext context) return context.RouteContext . HttpContext . Request . Headers[ " User-Agent " ] . Any(h => h . ToLower() . Contains(substring)); Это атрибут ограничения действия, который предотвращает соответствие запроса действиям, когда заголовок User-Agent не содержит указанную строку. Внутри ме­ тода Accept () из объект а HttpContext извлекаются заголовки НТТР и посредством LINQ выясняется , содержит ли какой-нибудь из них подстроку. которая получ ена че ­ рез конструктор. На заметку! Не полагайтесь на заголовок User-Agent при идентификации браузеров в ре­ альных прило жениях, потому что значения этого заголовка часто обманчивы . Например , версия браузера Microsoft Edge, которая являлась текущей на момент написания главы, отправляет заголовок User - Agent , содержащий Android, Apple, Chrome и Safari, из-за чего Microsoft Edge легко по ошибке принять за другой браузер. Более надеж­ ный подход предусматривает применение библиотеки JavaScript , такой как Modernizr (http://modernizr . com ), для обнаружения функциональных возможностей, от кото­ рых зависит приложение . В листинге 31.21 новое ограничени е применяется к одному из методов класса HomeController . Листинг 31 .21. Применение ограничения действия в файле HomeController. cs using ConventionsAndConstraints . Models ; using Microsoft . AspNetCore.Mvc ; using ConventionsAndConstraints.Infrastructure ; narnespace ConventionsAndConstraints .C ontrollers 11 [AdditionalActions] puЬlic class HorneController : Controller { puЫic IActionResult Index() => View( " Result ", new Result { Contro l ler = narneof(HorneController) , Action = narneof(Index) )); [ActionNarne( " Index " )] [UserAgent ( "Edge")] puЬlic IActionResul t Other () => View ( "R esul t ", new Resul t { Controller = nameof(HomeControl le r) , Action = narneof(Other) })i / / [ActionNarnePrefix ( " Do " ) ] [AddAction( " Details " )] puЫic IActionResult List() => View( " Result" , new Result { Controller = narneof(HorneController) , Action = nameof(List) })i
Глава 31. Соглашения по модели и ограничения действий 981 К методу Other () применяется атрибут Us erAgent , который указывает, что дейс­ твию не разрешено получать запросы с заголовком Use r-Ag e n t , не содержащим эле­ мент Ed ge . Запус тив приложение и запросив URL вида /Home/I n d e x в браузерах Goog l e Chrome и Microsoft Edge , вы заметите, что запросы обрабатываются разными методами (рис. 31.8). х L ______ Е1 RE!Stllt +- ·· • С : CJ loc all1o st:65325 locall1ost .653 25 1.· *' Co ntroller: Action: HomeController lndex Controller: Action : HomeController Other Рис. 31 .8 . Эффект от ограничения действия Влияние ограничения на выбор действия Предыдущий пример раскрыл один аспект, касающийся использования ограничений, ко­ торый может стать очевидным не сразу: действию с ограничением, чей метод Accept ( ) возвращает true для запроса, отдается предпочтение перед действием , к которому огра­ ничения вообще не применялись. Контроллер Home определяет два действия I ndex - созданные из методов I ndex () и Other () - и оба они могут применяться для обработки запросов, заголовок Us er-Agen t которых содержит элемент Edge . Причина использования для обработки запроса от брау­ зера Edge метода Other () связана с тем, что к нему было применено ограничение и метод Accept () этого ограничения возвращает t rue . Идея в том, что действие, которое имеет ограничение , принимающее запрос , является лучшим кандидатом, чем действие, вообще не имеющее ограничений. Соэдание сравнивающего ограничения действия Посредством свойств Cand idates и Curr entCandid ate объекта ActionCon st r aint Context огранич ения снабжаются деталями о других действиях, которые являются кандидатами на обработку запроса. Каждое потенциальное соответствие описывается с и с пользованием э кз емпляра класса Act i o n Se l ecto r Candidate , который опреде­ ляет свойства, приведенные в табл. 31.12. Таблица З 1.12. Свойств а класса ActionSelectorCandidate Имя Action Constraints Описание Это свойство возвращает объект ActionD escriptor, который описывает действие-кандидат Это свойство возвращает список объектов реализации IAct i onConstra int, содержащий набор ограничений, которые были применены к действию-кандидату
982 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Класс ActionDescriptor применяется для описания действия через свойства, перечисленные в табл . 31 .13 , многие из которых аналогичны свойствам, пр едостав ­ ляемым другими объектами контекста. Таблица 31.13. Избранные свойства класса ActionDescriptor Имя Name RouteConstraints Parameters Act ionConstraints Описание Это свойство возвращает имя действия Это свойство возвращает объект реализации IList<IRouteConstraintProvider>, который использу­ ется для ограничения нацеливания маршрутов на действие Это свойство возвращает объект реализации IList<PropertyModel>, который содер жит описания пара­ метров, требующихся методу действия Это свойство возвращает объект реализации IList<IActionConstraintMetadata>,coдepжaщий ограничения для данного действия Ограничения могут инспектировать действия-кандидаты и иметь сведения о том, как и где они были применены, что можно использовать для точной настройки их работы. В качестве примера рассмотрим способ, которым применяются ограничения к контроллеру Ноте в листинге 31.22. Листинг 31.22. Применение ограничения к контроллеру в файле HomeController. cs using ConventionsAndConstraints.Models; using Microsoft . AspNetCore . Mvc ; using ConventionsAndConstraints.Infrastructure ; namespace ConventionsAndConstraints . Controllers puЫic class HorneController : Controller ( puЫic IActionResult Index() = > Vi ew("Result ", new Result { Controller = nameof(HomeController), Action = nameof(Index) }); [ActionName ( "I ndex " )] [UserAgent("Edge")] puЬlic IActionResul t Other () = > View ( " Resu l t", new Resul t ( Controller = nameof(HomeController), Act i on = nameof(Other) 1); [UserAgent("Edge")] puЫic IActionResu l t Li st() => View( " Res ul t ", new Result ( Controller = nameof(HomeController) , Action = nameof(List) });
Глава 31. Соглашения по модели и ограничения действий 983 В приложении имеется только одно действие List и прим енени е к нему ограниче­ ния означает, что его могут использовать лишь запросы, чей заголовок User-Agent содержит элемент Edge. Например, если вы отправите запрос из браузера Chrome, то получите ответ 404 - Not Found . Пользы в этом мало, т.к. пользователи не поймут. почему они получили ошибку, из­ за отсутствия поясняющего текста, который бы предложил применить взамен другой браузер. Ограничения удобны, когда необходимо управлять выбором метода действия для обработки запроса, но не когда нужно полностью предотвратить использование специфического действия . Если вашей целью является как раз последнее, тогда при­ мен е ние фильтра позволит перенаправить клиента на описательную страницу ошиб­ ки, что будет намного более полезным ответом. Чтобы решить проблему, понадобится обновить класс UserAgentAttribute так, чтобы ограничение не отклоняло запросы, :когда для обработки запроса доступно только одно действие-кандидат (листинг 31.23). Листинг 31 .23. Проверка наличия других действий-кандидатов в файле UserAgentAttribute.cs using System ; us i ng System .Linq; using Microsoft.AspNetCore . Mvc . ActionConstraints ; namespace ConventionsAndConstraints . Infrastructure puЬlic class UserAgentAttribute : Attribute , IActionConstraint private string substring ; puЫic UserAgentAttribute(string sub) { substring = sub.ToLower() ; puЫic int Order {get; set; }= О; puЫic boo l Accept(ActionConstraintContext context) { return context . RouteContext.HttpContext . Request .He aders[ " User - Agent " ] . Any(h => h.ToLower() . Contains(substring)) 11 context.Candidates.Count() == 1; Дополнительный запрос LINQ проверяет, что действие- кандидат, возвращенное свойством CurrentCandidate, является единственным в :колле1щии, которую воз­ вратило свойство Candida t es. Если это так, тогда ограничение знает, что инфра­ структура MVC не располагает альтернативным действием, и разрешает обработку запроса. Результат можно просмотреть, запустив приложение и запросив URL вида /Home/List в браузере Google Chrome. Хотя заголовок User - Agent , отправляемый Chrome, не содержит элемент Edge, который указан с помощью атрибута для метода List (), класс ограничения выясняет. что другие действия-кандидаты отсутствуют, и позволяет обработать запрос.
984 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC Распознавание зависимостей в ограничениях действий Интерфейс I Ac tionCon st r aintFac t o r y используется, когда необходимо распоз ­ навать зависимости в ограничении действия поср едством поставщика служб, кото ­ рый был описан в главе 18 . Бот определение этого интерфейса: usi ng System; name s pace Microsoft. AspN etCore . Mvc . Ac t ionCo ns traints puЫ i c interface IActionCon straintFa ctor y : I Ac tionConstraintMetadata I Ac t ionCon strain t Create in stance (I Se r v i ceProvider services) ; bool IsReusaЬle { get; } М е тод Create in stance () вы з ывается для создания новых эк з емпляров кл асса ограничения действия, а свойство IsReus aЫ e применяется для указания , могут ли объекты, возвращаемые методом Createins t ance ( ),использоваться для множества з апросов . Для демонстрации применения интерфейса I Ac ti o n Con s traintFac t ory нуж­ на зависимость, которая потребует распознавания. С этой целью добавьте в папку Infrastructure файл класса по имени UserAgen t Comp a r er . cs , содержимое кото­ рого приведено в листинге 31.24 . Листинг 31.24. Содержимое файла UserAgentComparer. cs из папки Infrastructure usi ng System . Li nq; u sing Microsoft . Asp NetCore .H ttp ; namespace Convention sAndConstraints .In frastruc tu re pu Ьl ic class UserAgentComp are r { puЬlic bool Contai nsString(Htt pRequest r equest , stri ng agent) { string search Term = agent . ToLower( ); r e t urn reque st. Headers [" User- Agent "J . Any(h => h.ToLower() .Contai ns(searchTerm) ); В классе Use r Age n tComparer опр еделен единственный м етод . который ищет строку внутри заголовка User- Agent в НТГР-запросе. Это та же самая функциональ­ ность, которая использовалась ранее , но упакованная в отдельный класс , так что его жизненным циклом можно управлять с применением пост авщика служб. сконфигу­ рированного в листинге 31.25. Листинг 31.25. Регистрация типа для поставщика служб в файле Startup. cs u sing Mi crosoft . AspNetCore . Bu i lder ; using Microsoft . Extensions. Depende n cyi njection ; us i ng ConventionsAndConstra i nts .I n fr astructure ; name s pace Convent i o n sAndCo n straints p uЬlic class Startu p {
Глава 31 . Соглашения по модели и ограничения действий 985 puЫic void ConfigureServices(IServiceCol lection services) { services.AddSingleton<UserAgentComparer>(); servi ces . AddMvc() .AddMvcOpti ons(opti ons => { 11 options . Conventions . Add(new ActionNamePrefixAttr i b ut e( "D o " )) ; 11 options . Conven t ions . Add(new Additiona l ActionsAtt r ib u te()) ; }); puЫic void Confi gure ( IAppl icati onBui lde r арр) { app . UseStatusCodePages() ; app . UseDeve l ope r ExceptionPage() ; app .UseStati c Fi l es() ; app . UseMvcWithDefa u l t Route() ; Здесь выбран жизненный цикл одиночки, т.е. будет использов аться единственный экземпляр UserAgentComp a rer. В листинге 31 .26 показ ано обновл е нное ограничение UserAgent , которо е теп е рь делег и рует работу по инспектированию з аголовк а объекту UserAgen t Comparer , получаемому с помощью поставщика служ б. Листинг 31.26. Распознавание зависимостей в файле UserAgentAttribute. cs using System ; using System.Linq ; using Microsoft . AspNetCore . Mvc . Ac ti o n Constraints ; using Microsoft.Extensions . Dependencyinjection; namespace Conven ti onsAndConstra i nt s. Infrast r ucture puЬlic class UserAgentAttribute : Attribute, IActionConstraintFactory private string s ubstring ; puЫic UserAgentAttribute(string sub) { substring = sub; puЫic IActionConstraint Createinstance(IServiceProvider services) { return new UserAgentConstraint(services.GetService<UserAgentComparer>(), suЬstring) ; puЫic Ьооl IsReusaЫe => false; private class UserAgentConstraint : IActionConstraint private UserAgentComparer comparer; private string suЬstring; puЬlic UserAgentConstraint(UserAgentComparer comp, string suЬ) { comparer = comp; substring = suЬ.ToLower(); puЫic int Order { get; set; О;
986 Часть 11. Подробные сведения об инфрастру к туре ASP.NET Core MVC puЫic bool Accept(Act i onConstrai ntContext context ) return compa r er . Co n tainsString(con te xt . RouteCon t ext . Htt pContext . Re que st, substring) 11 context .Candidat es. Count () == 1; В приведенной модели атрибут. примененный к методам действий, отвеча­ ет за создание экземпляров класса ограничения, когда вызывается его м е тод Create i nstance () . В качестве аргумента м етод Create i nst a nce () получает объ­ ект реализации I Servi ceP rovide r, используемый в примере для получения объекта Us e r AgentCompare r, поэтому можно создавать экземпляр закрытого класса ограни­ чения, который затем будет зад ействован в процессе выбор а. Избежание ловушки, связанной с областью действия Подобно другим средствам, основанным на атрибутах , применение атрибута ограничения к классу контроллера эквивалентно применению данного атрибута к каждому отдельному методу. Тем не менее, обычно это приводит к нежелательным результатам, поскольку целью ограничений является помощь инфраструктуре MVC в выборе метода действия, а примене­ ние одного и того же ограничения ко всем действиям контроллера в целом не способствует достижению указанной цели. Например, если применить атрибут Us erAge nt к классу HomeControl l er , то действия Index перестанут быть достижимыми из любого браузера . Оба действия Index будут оди­ наково подходящими для браузеров, которые включают в заголовок User -Agent элемент Edge , что в итоге даст исключение. Всем остальным браузерам не подойдет ни одно из действий Index , что в результате приведет к ответу 4 04 - Not Found . Внутри ограничения можно было бы воспользоваться объектом контекста, чтобы найти дру­ гие ограничения и посмотреть, отклонят ли они запрос, но тогда метод Accept ( ) каждого ограничения будет многократно вызываться для каждого запроса , что окажется затратным процессом, которого лучше избегать. Ограничения хорошо работают в ситуации, когда есть множество методов действий , кото­ рые могут обработать один и тот же запрос, из-за чего к ним и применяется ограничение . Резюме В настоящей главе были описаны два средства, которые используются для на­ стройки работы MVC. Вы узнали, как применять соглашения по модели для изменения способа отображения классов и методов на контроллеры и действия . Кроме того. было показано, каким образом использовать ограничения действий для сужения диап азо­ на запросов, которые действие может обрабатывать, и как их применять для выбора действия из списка кандидатов , идентифицированных при поступлении запроса. Итак. изучение инфраструктуры ASP.NET Core MVC завершено. Оно начиналось с создания простого приложения, после чего вы совершили комплексный тур по разно­ образным компонентам в инфраструктуре, научившись их конфигурировать, настра­ ивать или полностью заменять . Желаю вам успехов в разработке собств енных при­ ложений MVC и надеюсь на то, что вы получили не меньшее удовольстви е от чте ния этой книги, чем я во время ее написания .
Предметный указатель А Ajax (Asynchronous JavaSciipt and ХМL). 631 Arrange/act/assert (А/А/А). 179 АSР.NЕТСоге, 355 ASP.NEТ Соге Identity, 325 ASP.NEТ Core Web Application (.NЕТ Core). 140 ASP.NET Саге Web Application (.NET Framework). 140 ASP.NEТWeb Forms, 21 в Browser Link (Ссылка на браузер), 156 с CDN (Content delivery network). 776 CRUD (Create, read. update, delete), 201: 302 CSRF (Cross-site request fогgегу) , 7 46 D DAL (Data access lауег). 73 Dl (Dependency injection). 543 Е Entity Framework Core (EF Core), 218 G Git, 350 GU!D (Globally unique identifier), 566 J JavaScript, 161 JSON (JavaScript Object Notation), 377: 535;629 к Kestrel, 384 L LINQ (Language Integrated Query). 26 LocalDB. 363 LТS (LongThrm Support), 349 м МVС (Model-view-controller), 20; 24 МVР (Model-view-presenter). 74 МWМ (Model-view-view model), 75 N . NEТCore, 140; 351 Node.js. 349: 351 Node Package Manager (NPM), 351 NuGet. 204 о ОRМ (Object -relational mapping). 218 R Razor. 115: 147; 654 s SportsStore, 201: 244: 276: 298: 325 т TDD (Thst-Diiven Development), 189 u URL (Uniform resource locator), 25 относительные к приложению. 789 v Visual Studio. 151 Visual Studio Code. 348; 353 х XUnit.net. 175 А Администрирование,298 защита средств администрирования. 325 Архитектура МVС, 24 "модель-представление", 73 Атака CSRF, 7 4 7 База данных Identity. 326 Б добавление пакетов в базу данных, 364 миграции баз данных, 225: 343 переустановка базы данных. 289 расширение базы данных. 288 создание,337:363 классов базы данных, 220
988 Предметный указатель в Веб-сервер Kestrel, 384 Виджет, 281 Визуализация представлений. 659 Внедрение в действия, 573 Внедрение в свойства. 573 Внедрение зависимостей (DI), 51; 543; 550:556 для фильтров, 600 использование для конкретных типов. 563 конфигурирование, 559 Выражение Razor. 128 лямбда. 101 Генерация URL. 478 г списка категорий, 255 д Данные добавление данных модели представления, 232 создание формы для ввода данных. 172 Действия,500 Диспетчер пакетов Node (NPM), 351 NuGet, 141; 197; 225 Запрос Ajax, 631 з использование тел запросов в качестве источников данных привязки, 824 подделка межсайтовых запросов (CSRF), 746 Зтцита средств администрирования, 325 и Идентификатор (GUID), 566 Инициализатор коллекции, 95 объекта,94 Инструмент Bower, 143; 351 Gulp. 146 NPM, 146 NuGet, 141 Интерполяция строк, 93 Инфраструктура имитации. 196 к Категория генерация списка категорий, 255 Кеш, 788 аннулирование кеша, 775 данных, 783 установка времени истечения для кеша. 786 Кеширование, 783 Класс ActionExecutedContext. 589 AddressSummary, 809 Anchor'lagHelper. 781 AppldentityDbContext, 327 ApplicationDbContext, 362 Appointment, 835 Assert, 180 CacheThgHelper, 788 Cart, 263; 276 CartController, 279 Controller, 443; 508; 520; 590 CustomeгController, 461 CustomHtmlResult, 519 DbContext. 220 DiagnosticsFilter, 604 DictionaryStorage. 561 EFRepository, 362 EnvironmentтagHelper. 768 ErrorMiddleware. 399 ExceptionF!lterAttribute. 599 FoгmThgHelper, 745 GuestResponse, 361 HeaderModel, 823 HomeController, 408; 430 HttpContext. 391 ImageThgHelper. 782 LegacyRoute, 482; 484; 489 LinkТag, 779 LinkThgHelper, 778; 779 MemoryRepository. 547 Mock, 200 ModelExpression. 735 ModelStateD!ctionary, 836 ModelValidationContext, 853 MvcRouteHandler, 487 OrderController, 291 PocoController, 506 Product, 182 Program, 374; 383 RazorV!ewEngineOptions . 671 ResultF!lterAttribute. 593 ScriptThgHelper, 769 Startup, 207; 268; 374; 385; 429; 578 StartupDevelopment, 425 StatusCodeResult, 541 ThgHelper, 716 ThgHelperContent, 728 ThgHelperOutput, 728 ThxtAreaThgHelper, 763 ТimeFilter, 603 ТypeBroker, 553 ValidationSummaryThgHelper. 841 ViewComponent. 257 ViewResult. 520
VJewResнltDetailsAttribute, 594 VirtuaJPathContext, 490 дескрипторный вспомогательный, 229; 708;719;764 длл проверки достоверности форм, 764 использование . 741 соэдание, 715 усовершенствованные возможности, 726 контекста,326 соэдание класса корзины, 276 хранилища , 221 базы данных, 220 Коллекцил инициализатор коллекции, 95 привлзка коллекций, 815 сложных типов, 81 7 применение коллекции в качестве типа модели,816 Компоненты представлений,252;686 РОСО, 686 соэдание, 686 приложен.ил МVС. 85 создание. 85 слабо свлэанные, 211 создание, 549 Компоновки, 124 создание, 123 Контроллер,33;70;500 Account. 332 АР!, 614: 623 CRUD. 303 для отображения сообщений об ошибках , 338 добавление контроллера, 213 создание. 117: 139;365: 617: 682;711;831 Конфигурацил маршрутизации, 212 Конфигурирование внедрения зависимостей, 559 поставщика служб. 558 приложенил,327;369;713 сериалиэатора JSON, 637 служб мvс. 419 приложения. 345 эле ментов input, 7 49 Корзина добавление виджета с итоговой информа­ цией по корзине. 281 создание класса корзины, 276 службы корзины, 278 удаление элементов из корзины, 279 л Лямбда-выражение, 101 Предметный указа т е л ь 989 м Маршрут, 36 добавление нового маршрута. 469 ограничение маршрутов, 451 на основе типов и значений, 455 с использованием регулярного выраже- нил. 454 переменной длины, 448 применение ограничений к маршрутам, 463 сложный. 463 Маршрутизация, 400 ASP.NEТ, 236 URL, 26; 426 входящих URL, 481 генерация URL, 4 78 конфигурация маршрутизации. 212 стандартная.448 на контроллер . 486 на контроллеры МVС. 485 на основе соглашений, 426; 460 настройка системы маршрутизации, 4 79 помещение маршрутизации в контекст, 427 создание простого маршрута, 434 с помощью атрибутов , 426; 460 упорядочение маршрутов, 440 Массив привязка массивов . 813 Мет оды асинхронные, 109 расширяющие, 97 фильтрующие, 100 Механизм Razor, 654; 657; 658; 671 виэуалиэации,644 Миграция, 225 Минификацил, 166 Модель, 43 создание модели, 545 представлений. 69 привязка модел ей, 50; 270; 793; 799 стандартные эначенил привязки , 801 указание источника данных привязки моделей, 820 проверка достоверности модели, 314: 828; 834 конфигурирование стандартных сообще­ ний об ошибках проверки достовер­ ности, 842 правила проверки достоверности с помо ­ щью метаданных . 849 на стороне клиента. 854 на стороне сервера, 854 создание класса модели, 284 модели. 710 Модель-представление-контроллер ( МVС ) , 24; 68
990 Предметный указатель Модель-представление-модель пр едстав­ ления (МVVМ). 75 Модель-представление-презентатор (MVP). 74 Модель представления, 734 арх:итектура , 73 Модульное тестирование. 201; 230: 233 мvс. 170; 174 Visual Studio Code , 369 действий , 519 контроллера с з ависимостью, 560 контроллеров,519 создание проект а модульного тестирова­ ния. 548 фильтров, 587 Модульные тесты написание и выполнение, 178 о Области , 426; 491 влияние области на приложение МVС, 495 генерирование ссылок на действия, 496 Объединение ограничений, 456 Объект инициализатор объекта , 94 Операция CRUD , 302 null-условная операция(?). 87 объединения с null (??), 89 Организация/ действи е /утверждение (arrange/act/assert), 179 Отладчик Visual Studio, 151 Ошибки проверки достоверности, 842 конфигурирование стандартных сообще­ ний. 842 Пакет Bootstrap, 238 Git, 350; 351 Identity, 325 Moq , 196 п NuGet, 204; 357; 414 Уе ошаn , 355 Уеошаn (уо), 351 диспетчер пакетов NuGet. 141 управление программными пакетами , 141 Пакетирование, 166 Паттерн МVС,68 REST,625 интеллектуальный пользовательский ин­ терфейс (Sш art UI) , 71 локатор служб (Service Locator), 575 модель-представление-модель пр едставле­ ния (МVVМ). 75 модель-представление - презентатор (МVР), 74 Платформа ASP.NEТ Core , 23 Политика безопасности, 336 Представление.37;70:332:644 Razor, 71 ; 147 визуализация представлений, 659 для отображения сообщений об ошибках . 338 добавлени е и конфигурирование. 215 компонентыпредставлений.252;678:686 обновление представлений, 164 подготовка представлений, 503 создание,38; 118; 139;255;365; 431;682 гибридных представлений. 704 навигационного компонента представления , 252 строго типизированное, 120: 524 тестирование представлений, 40 файл запуска представления. 126 частичное,241;666;686 Привязка атрибуты для источников данных привяз­ ки. 820 моделей,50;270;793;799 указани е источника данных привязки моделей,820 простых типов, 802 сложных типов, 803 из заголовков, 823 свойств избирательная , 811 стандартные значения привя з ки, 801 Приложение мvс минификация, 166 модульное тестирование, 170; 174 пак етирование. 166 SportsStore, 241 конфигурирование, 223; 327; 340; 369: 619; 713;798 службы приложения . 345 развертьmание, 336; 343 Проект АSР. NЕТСоrе, 355 р Разработка через тестирование (TDD), 189 Редактор Visual Studio Code. 353; 361: 365: 372 Сегмент. 433 URL с необязательный , 446 Сеть доставки содержимого (CDN), 776 Система управления базами данных SQLite, 363
Служба, 386 ASP.NEт. 387 мvс. 419 жизненный цикл службы, 565 конфиrурирование поставщика служб , 558 регистрация службы, 277 хранилища, 211 соэдание службы корзины, 278 Среда .NEТCore. 140; 351 Node .js, 349; 351 Visual Studio, 151 Ubuntu, 349; 351 Средство BrowserLink, 156; 159 Ссылка генерация ссылок на категории с помощью компонента представления,256 исходящих, 468 на метод действия, 46 отображение ссылок на страницы, 228; 234 создание ссылок на страницы , 230 Стилизация представления , 63; 65 Страница разбиение на страницы , 226 редактирования,302 списка.302 Строка интерполяция строк, 93 подключения , 222 Схема URL, 432 т Таблица стилей CSS, 60; 778 создание , 163 Тест избирательный прогон тестов, 182 модульный создание. 371 параметризованный, 191 прогон тестов, 371 Тестирование модульное, 201; 230; 233 Технология ASP.NEТWeb F'orms, 21 тип анонимный, 108 ТИпизация неявная, 107 Точка останова, 152 у Управление программными пакетами, 141 Уровень доступа к данным (DAL), 73 Файл CSS, 161 JavaScript, 161 Предметный указатель 991 ф запуска представления, 126 импортирования представлений. 121 Фильтрация по категории, 248 Фильтры, 576; 581 авторизации,584:585 асинхронные, 591: 595 гибридные, 596 глобальные, 608 действий.584:588:590 исключений,584;598;599;601 порядок применения фильтров, 610 изменениепорядка,612 результатов, 584; 592 с зависимостями. 605 Формат JSON (JavaScript Object Notation), 379; 535; 629 х Хранилище, 545 класс хранилища, 221 соэдание , 221 фиктивное соэдание , 210 ц Цепочки зависимостей, 560 Шаблон URL, 432 проекта ш ASP.NEТCoreWebApplication ( . NEТCore), 140 ASP.NEТCore Web Applicatioп (.NЕТ F'ramework), 140 формирование шаблонов (scaffolding), 212 универсализации имен, 770 Элемент img, 782 input, 749 label. 754 option. 756 select. 756; 758 textarea, 763 якорный, 781 э
ЯЗЫК ПРОГРАММИРОВАНИЯ С# б.О И ПЛАТФОРМА .NET 4.б 7-Е ИЗДАНИЕ Эндрю Троелсен, Филипп Джепикс www.williamspuЬlishing.com ISBN 978-5 -8459-2099-7 Новое 7-е издание этой книги было ПОЛНОСТЬЮ пересмотрено и переписано с учетом последних изменений спецификации языка С# и дополнений платформы .NET FгamewOl'k. Отдельные главы посвящены важным новым средствам, которые делают .N ET FгamewOl'k 4.6 самым передовым выпуском, в том числе: усовершенствованная модель программирования ADO.NET Entity Fгamework; многочисленные улучшения IDЕ-среды и архитектуры MVVM для разработки настольных приложений WPF; многочисленные обновления в ASP.NET Web API. Помимо этого, предлагается исчерпывающее рассмотрение всех ключевых возможностей языка С#, как старых, так и новых, что позволило обрести популярность предыдущим изданиям этой книги. Читатели получат основательные знания приемов объектно­ ориентированной обработки , атрибутов и рефлексии, обобщений и коллекций, а также обретут понимание МНОГИХ сложных тем. в продаже
www.dialektika.com лpress ® 9 785990 891043 www.apress .com