ASP.NET Core MVC 2 с примерами на С# для профессионалов 7-е издание ProASP.NET CoreMVC2 Seventh Edition Adam Freeman лpress® ASP.NET Core MVC 2 с примерами на С# для профессионалов 7-е издание Адам Фримен 11)~~ 6 NIДl1aQilUl(4 Москва • Санкт-Петербург 2019 ББК 32.973.26-018.2.75 УДК 681.3.07 Ф88 ООО "Диалектика" Перевод с английского и редакция Ю.Н. Артеменко По общим вопросам обращайтесь в издательство "Диалектика" по адресу: [email protected], http://www.dialektika.com Фримев, Адам. Ф88 ASP.NET Core MVC 2 с примерами на С# для - СПб.: ООО "Диалектика'', 2019. - 1008 англ. ISBN 978-5-6041394-3-1 профессионалов. 7-е изд. с.: ил. - : Пер. с Парал. тит. англ. (рус.) ББК 32.973.26-018.2.75 Все названия программных прод.уктов являются зарегистрированными торговыми мар­ ками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет APress, Berkeley, СА. Copyright © 2019 Ьу Dialektika Computer PuЬlishiпg. Authorized translation from the English language editioп puЬlished Ьу APress. lnc., Copyright © 2017 Ьу Adam Freeman. This translation is puЬlished Ьу arrangement with APress, Berkeley, СА. All rights are reserved Ьу the PuЬlisher, whether the wl10le or part of tl1e material ls coпcerned, specifically the rigl1ts of translation, reprinting, reuse of illustratioпs, recitatioп, broadcasting, reproduction оп microf11ms or in any other physical way, and transmission or information storage and retrieval, electronic adaptation, computer software. or Ьу similar or dissimilar methodology now known or hereafter developed. Тrademarked names, logos, and images may appear in this book. Rather than use а trademark symbol with every occurrence of а trademarked name, logo, or image we t1se the names, logos, and images only in an editorial fashion and to the benefit of the trademark owner, with по intention of infringement of the trademark. письменного разрешения издательства Научно-популярное uздшше АдамФримев ASP.NET Core МVС 2 с примерами на С# для профессионалов 7-е издание Подписано в печать 28.09.2018. Гарнитура Формат 70х100/16 Times Усл. печ. л. Тираж 81,27. Уч.-изд. л. 56.6 400 экз. Заказ М 10168 Отпечатано в АО "Первая Образцовая типография" Филиал "Чеховский Печатный Двор" 142300, Сайт: Московская область, г. Чехов, ул. Полиграфистов. д. www.chpd.ru, E-mail: [email protected], ООО "Диалектика", 195027, ISBN 978-5-6041394-3-1 ISBN 978-1-4842-3149-4 (рус.) (англ.) тел. 8 (499) 270-73-59 Санкт-Петербург, Магнитогорская ул" д. 30. лит. А. пом. 848 © ООО "Диалектика", 2019 © Ьу Adam Freeman, 201 7 Оглавление Часть Введение в инфраструктуру ASP.NET 1. Core MVC 2 21 Глава 1. Основы ASP. NET Core MVC Глава 2. Ваше первое приложение Глава 3. Паперн, проекты и соглашения Глава 4. Важные функциональные возможности языка С# Глава 5. Работа с Razor 122 Глава 6. Работа с Visual Studio 143 Глава 7. Модульное тестирование приложений MVC 176 Глава 8. SportsStore: Глава 9. SportsStore: навигация Глава 10. SportsStore: завершение построения корзины для покупок 281 Глава 11. SportsStore: администрирование 303 Глава 12. SportsStore: защита и развертывание 330 22 MVC 31 MVC 72 реальное приложение 205 249 Глава 13. Работа с Visual Studio Code Часть 11. 87 Подробные сведения об инфраструктуре 356 ASP.NET Core MVC 2 379 Глава 14. Конфигурирование приложений 380 Глава 15. Маршрутизация 435 Глава 16. Дополнительные Глава 17. Контроллеры Глава 18. URL возможности маршрутизации и действия 477 513 Внедрение зависимостей 556 Глава 19. Фильтры 589 Глава 20. Контроллеры Глава 21. Представления 658 Глава 22. Компоненты представлений 693 Глава 23. Вспомогательные функции дескрипторов 723 Глава 24. Использование вспомогательных функций дескрипторов для форм 755 Глава 25. API 627 Использование других встроенных вспомогательных функций дескрипторов 779 809 Глава 26. Привязка моделей Глава 27. Проверка достоверности моделей 844 Глава 28. Введение в 877 Глава 29. Применение Глава 30. Расширенные средства Глава 31. Соглашения по модели и ограничения действий ASP.NET Core ldeпtity ASP.NET Core ldentity Предметный указатель ASP. NET Core ldentity 913 941 973 1оо1 Содержание Об авторе Часть Глава 19 Введение в инфраструктуру 1. ASP.NET Core MVC 2 1. Основы ASP.NET Core MVC История развития ASP.NEТ ASP.NEТWeb 22 Core МVС 22 Forms Первоначальная инфраструктура МVС Framework Обзор ASP.NEТ Core Что нового в ASP. NЕТ Core МVС 2? Основные преимущества ASP. NЕТ Core МVС Что необходимо знать? Какова структура книги? Часть 1. Введение в инфраструктуру ASP.NEТCore МVС Часть 11. Подробные сведения об инфраструктуре ASP.NET Core МVС [Де можно получить код примеров? Резюме Глава 2. Ваше первое приложение 21 MVC 23 24 25 26 26 29 30 30 30 30 30 31 Установка Visual Studio Установка .NЕТ Core 2.0 SDK Создание нового проекта ASP.NET Core МVС Добавление контроллера Понятие маршрутов Визуализация веб-страниц Создание и визуализация представления Добавление динамического вывода Создание простого приложения для ввода данных 31 32 32 37 39 39 39 43 44 Предварительная настройка 44 Проектирование модели данных Ссьшка на методы действий 46 46 48 Построение формы 49 Получение данных формы 51 59 65 Создание второго действия и строго типизированного представления Добавление проверки достоверности Стилизация содержимого Резюме 71 Глава З. Паттерн, проекты и соrлашения История создания MVC MVC Особенности паттерна MVC Понятие моделей Понятие контроллеров Понятие представлений Реализация паттерна МVС в ASP.NEТ Core Сравнение МVС с другими паттернами Паттерн "Интеллектуальный пользовательский интерфейс" 72 72 72 73 74 74 74 76 76 Содержание 80 80 ПроектыАSР.NЕТСоrе МVС Создание проекта Соглашения в проекте МVС 84 86 Резюме Глава 4. 7 Важные функциональные возможности языка С# 87 Подготовка проекта для примера Включение ASP.NET Core МVС Создание компонентов приложения МVС Использование null-условной операции Связывание в цепочки null-условных операций Комбинирование null-условной операции и операции объединения с null Использование автоматически реализуемых свойств Использование инициализаторов автоматически реализуемых свойств Создание автоматически реализуемых свойств только для чтения Использование интерполяции строк Использование инициализаторов объектов и коллекций Использование инициализатора индексированной коллекции 88 89 89 91 92 94 95 95 96 98 99 100 1О1 Сопоставление с образцом Сопоставление с образцом в операторах swi tch Использование расширяющих методов 102 103 Применение расширяющих методов к интерфейсу 105 Создание фильтрующих расширяющих методов 106 108 Использование лямбда-выражений Определение функций 109 Использование методов и свойств в форме лямбда-выражений 112 114 114 116 Использование автоматического выведения типа и анонимных типов Использование анонимных типов Использование асинхронных методов Работа с задачами напрямую Применение ключевых слов async и awai t Получение имен Резюме Глава 5. Работа с Razor Определение модели Создание контроллера Создание представления Работа с объектом модели Использование файла импортирования представлений 123 124 124 125 126 128 129 Работа с компоновками Создание компоновки Применение компоновки Использование файла запуска представления Razor Вставка значений данных Установка значений атрибутов Использование условных операторов Проход по содержимому массивов и коллекций Резюме 118 120 121 122 Подготовка проекта для примера Использование выражений 11 7 130 132 133 135 136 137 138 140 142 8 Содержание Глава 6. Работа с Visual Studio 143 Подготовка проекта для примера Создание модели Создание контроллера и представления Управление программными пакетами Инструмент Инструмент NuGet Bower Итеративная разработка Внесение изменений в представления Razor Внесение изменений в классы С# Browser Link JavaSciipt и CSS для развертывания Использование средства Подготовка файлов Включение доставки статического содержимого Добавление в проект статического содержимого Обновление представления Пакетирование и минификация в приложениях МVС Резюме Глава 7. Модульное тестирование приложений MVC 176 Подготовка проекта для примера Включение встроенных вспомогательных функций дескрипторов Добавление действий к контроллеру Создание формы для ввода данных Обновление представления Index Модульное тестирование приложений МVС Создание проекта модульного тестирования Создание ссылки на проект Изолирование компонентов для модульного тестирования Улучшение модульных тестов Параметризация модульного теста Улучшение фиктивных реализаций Резюме Глава 8. SportsStore: реальное приложение Создание проекта МVС Создание проекта модульного тестирования Проверка и запуск приложения Начало работы с моделью предметной области Создание хранилища Создание фиктивного хранилища Регистрация службы хранилища Отображение списка товаров Добавление контроллера Добавление и конфигурирование представления Установка стандартного маршрута Запуск приложения Подготовка базы данных Создание классов базы данных Создание класса хранилища 177 177 178 178 179 180 181 182 187 195 195 199 204 205 Начало работы Установка пакета инструментов командной строки 143 144 145 146 14 7 149 153 153 154 162 167 168 168 170 172 175 Entity Framework Core 206 206 209 21 1 212 2 12 2 13 214 215 216 21 7 219 220 221 22 1 222 223 Содержание Определение строки подключения Конфигурирование приложения Создание миграции базы данных Создание начальных данных Добавление поддержки разбиения на страницы Отображение ссылок на страницы Улучшение URL Стилизация содержимого Установка пакета Bootstrap Bootstrap к компоновке Применение стилей Создание частичного представления Резюме Глава навиrация 9. SportsStore: Фильтрация списка товаров URL Построение меню навигации по категориям Корректировка счетчика страниц Построение корзины ДJIЯ покупок Определение модели корзины Создание кнопок добавления в корзину Включение поддержки сеансов Реализация контроллера ДJIЯ корзины Отображение содержимого корзины Резюме Глава 1О. SportsStore: завершение построения корзины для покупок Усовершенствование модели корзины с помощью службы Создание класса корзины. осведомленного о хранилище Регистрация службы Упрощение контроллера Cart Завершение функциональности корзины Удаление элементов из корзины Добавление виджета с итоговой информацией по корзине Отправка заказов Создание класса модели Добавление реализации процесса оплаты Реализация обработки заказов Завершение построения контроллера Order Отображение сообщений об ошибках проверки достоверности Отображение итоговой страницы Резюме Глава 11. SportsStore: администрирование Управление заказами Расширение модели Добавление действий и представления Добавление средств управления каталогом Создание контроллера CRUD Реализация представления списка 223 224 227 227 231 232 241 242 243 243 246 248 249 Добавление навигационных элементов управления Улучшение схемы 9 249 249 253 257 265 267 268 271 273 274 277 280 281 281 281 282 283 284 284 286 289 289 290 293 297 300 301 302 303 303 303 304 307 308 309 1О Содержание Редактирование сведений о товарах 311 324 326 329 Создание новых товаров Удаление товаров Резюме Глава 12. SportsStore: защита и развертывание 330 Защита средств администрирования Резюме 330 330 335 337 341 341 341 343 34 7 347 351 355 Глава 356 Создание базы данных Iden ti ty Применение базовой политики авторизации Создание контршшера Accoun t и представлений Тестирование политики безопасности Развертывание приложения Создание баз данных Подготовка приложения Применение миграций к базам данных Управление начальным заполнением баз данных Развертывание приложения 13. Работа с Visual Studio Code Настройка среды разработки Установка Проверка установки Установка 356 356 358 358 359 359 359 360 360 361 361 362 363 364 366 366 367 367 370 372 376 377 378 378 Node.js Node Git Проверка установки Git Установка Bower Установка .NЕТ Core Проверка установки .NЕТ Core Visual Studio Code Проверка установки Visual Studio Code Установка расширения С# для Visual Studio Code Создание проекта ASP.NET Core Подготовка проекта с помощью Visual Studio Code Установка Управление пакетами клиентской стороны Конфигурирование приложения Построение и запуск проекта Воссоздание приложения Partylnvi tes Создание модели и хранилища Создание базы данных Создание контршшеров и представлений Модульное тестирование в Visual Studio Code Создание модульного теста Прогон тестов Резюме Часть Глава 11. Подробные сведения об 14. Конфигурирование инфраструктуре приложений Подготовка проекта для примера Конфигурирование проекта Добавление пакетов в проект Добавление пакетов инструментов в проект ASP.NET Core MVC 2 379 380 382 383 384 386 Содержание Класс Program 11 Резюме 387 388 391 394 397 407 41 1 416 418 420 421 426 427 428 429 430 432 434 Глава 435 Анализ деталей конфигурации Класс Startup Службы ASP.NET Промежуточное программное обеспечение Особенности вызова метода ASP.NET Conf igure () Добавление оставшихся компонентов промежуточного программного обеспечения Конфиrурирование приложения Создание конфиrурационного файла JSON Использование данных конфигурации Конфигурирование регистрации в журнале Конфигурирование внедрения зависимостей Конфиrурирование служб МVС Работа со сложными конфиrурациями Создание разных внешних конфиrурационных файлов Создание разных методов конфиrурирования Создание разных классов конфигурирования 15. Маршрутизация URL Подготовка проекта для примера Создание класса модели Создание примеров контроллеров Создание представления Введение в шаблоны URL Создание и регистрация простого маршрута Определение стандартных значений Определение встраиваемых стандартных значений Использование статических сегментов URL Определение специальных переменных сегментов Использование специальных переменных в качестве параметров метода действия Определение необязательных сегментов URL Определение маршрутов переменной длины Ограничение маршрутов Ограничение маршрута с использованием реrулярного выражения Использование ограничений на основе типов и значений Объединение ограничений Определение специального ограничения Использование маршрутизации с помощью атрибутов Подготовка для маршрутизации с помощью атрибутов Применение маршрутизации с помощью атрибутов Применение ограничений к маршрутам Резюме Глава 16. Дополнительные возможности маршрутизации Подготовка проекта для примера fенерирование исходящих URL в представлениях rенерирование исходящих ссьшок rенерирование rенерирование URL (без ссылок) URL в методах действий 437 437 438 439 440 442 443 444 447 452 454 455 458 460 464 465 466 468 4 71 4 71 472 475 476 477 4 78 4 79 480 490 491 Содержание 12 Настройка системы маршрутизации Резюме 492 492 493 496 497 504 505 505 506 509 510 510 512 512 Глава 513 Изменение конфигурации системы маршрутизации Создание специального класса маршрута Применение специального класса маршрута Маршрутизация на контроллеры МVС Работа с областями Создание области Создание маршрута для области Заполнение области Генерирование ссылок на действия в областях URL URL чистыми и понятными человеку POST: выбор правильного запроса Полезные советы относительно схемы Делайте GET и 17. Контроллеры и действия Подготовка проекта для примера 514 515 517 Подготовка представлений Понятие контроллеров Создание контроллеров 518 518 Создание контроллеров РОСО Использование базового класса Controller Получение данных контекста Получение данных из объектов контекста Использование параметров метода действия Генерирование ответа Генерирование ответа с использованием объекта контекста Понятие результатов действий Генерирование НТМL-ответа Передача данных из метода действия в представление Выполнение перенаправления Возвращение разных типов содержимого Реагирование с помощью содержимого файлов Возвращение ошибок и кодов НТТР Другие классы результатов действий Резюме Глава 18. Внедрение зависимостей Подготовка проекта для примера Создание модели и хранилища Создание контроллера и представления Создание проекта модульного тестирования Создание слабо связанных компонентов Исследование сильно связанных компонентов Введение в средство внедрения зависимостей ASP.NEТ Подготовка к внедрению зависимостей Конфигурирование поставщика служб Модульное тестирование контроллера с зависимостью Использование цепочек зависимостей Использование внедрения зависимостей для конкретных типов 521 522 522 527 528 529 530 532 535 541 548 551 552 555 555 556 557 558 559 561 561 562 568 569 570 572 573 575 Содержание :жизненные ЦИIUIЫ служб Использование переходного жизненного ци1U1а Использование жизненного ци1U1а, ограниченного областью действия Использование жизненного ци1U1а одиночки Использование внедрения в действия Использование атрибутов внедрения в свойства Запрашивание объекта реализации вручную Резюме Глава 19. Фильтры Подготовка проекта Д11Я примера ВIUlючение SSL Создание контроллера и представления Использование фильтров Понятие фильтров Получение данных контекста Использование фильтров авторизации Создание фильтра авторизации Использование фильтров действий Создание фильтра действий Создание асинхронного фильтра действий Использование фильтров результатов Создание фильтра результатов Создание асинхронного фильтра результатов Создание гибридного фильтра действий/ результатов Использование фильтров ис1U1ючений Создание фильтра ис1U1ючений Использование внедрения зависимостей Д11Я фильтров Распознавание зависимостей в фильтрах Управление жизненными ци1U1ами фильтров Создание глобальных фильтров Порядок применения фильтров и его изменение Изменение порядка применения фильтров Резюме Глава 20. Контроллеры API Подготовка проекта Д11Я примера Создание модели и хранилища Создание контроллера и представлений Роль контроллеров REST Проблема скорости Проблема эффективности Проблема открытости Введение в REST и контроллеры API Создание контроллера API Тестирование контроллера API Использование контроллера API в браузере Форматирование содержимого Стандартная политика содержимого Согласование содержимого Указание формата данных Д11Я действия 13 577 579 583 585 585 587 587 588 589 590 591 591 593 596 597 598 599 601 602 604 605 606 608 609 611 612 614 614 618 621 624 626 626 627 628 628 630 633 633 634 635 635 637 641 645 648 648 649 652 Содержание 14 Получение формата данных из маршрута или строки запроса Включение полного согласования содержимого Получение разных форматов данных Резюме Глава 21. Представления Подготовка проекта для примера Создание специального механизма визуализации I Vi ew IViewEngine Создание специальной реализации интерфейса Создание реализации интерфейса Регистрация специального механизма визуализации Тестирование механизма визуализации Работа с механизмом Razor Подготовка проекта для примера Прояснение представлений Razor Добавление динамического содержимого к представлению Razor Использование разделов компоновки Использование частичных представлений Добавление содержимого JSON в представления Razor Конфигурирование механизма Расширители местоположений представлений Резюме Глава 22. Компоненты представлений Подготовка проекта для примера Создание моделей и хранилищ Создание контроллера и представлений Конфигурирование приложения Понятие компонентов представлений Создание компонента представления Создание компонентов представлений РОСО Наследование от базового класса ViewComponent Понятие результатов компонентов представлений Получение данных контекста Создание асинхронных компонентов представлений Создание гибридных классов контроллеров и компонентов представлений Создание гибридных представлений Применение гибридного класса Резюме Глава 23. Вспомогательные функции дескрипторов Подготовка проекта для примера Создание модели и хранилища Создание контроллера, компоновки и представлений Конфигурирование приложения Создание вспомогательной функции дескриптора Определение класса вспомогательной функции дескриптора Регистрация вспомогательных функций дескрипторов Использование вспомогательной функции дескриптора Управление областью действия вспомогательной функции дескриптора 653 655 656 657 658 659 660 662 663 664 665 667 668 670 674 675 680 683 685 686 692 693 694 694 696 699 700 701 701 703 704 710 715 718 719 720 722 723 724 725 725 728 729 729 733 734 736 Содержание Усовершенствованные возможности вспомогательных функций дескрипторов Создание сокращающих элементов Вставка перед и после содержимого и элементов 15 740 741 7 43 Получение данных контекста представления и использование внедрения 747 7 49 751 753 754 зависимостей Работа с моделью представления Согласование вспомогательных функций дескрипторов Подавление выходного элемента Резюме Глава 24. Использование вспомогательных функций дескрипторов дпя форм Подготовка проекта для примера Переустановка представлений и компоновки Работа с элементами f о rm Установка цели формы Использование средства противодействия подделке Работа с элементами inpu t Конфигурирование элементов input Форматирование значений данных Работа с элементами Работа с элементами label select и option Использование источника данных для заполнения элемента s е 1 ее t option из перечисления textarea генерирование элементов Работа с элементами Вспомогательные функции дескрипторов для проверки достоверности форм Резюме Глава 25. Использование других встроенных вспомогательных функций дескрипторов Подготовка проекта для примера Использование вспомогательной функции дескриптора для среды размещения Использование вспомогательных функций дескрипторов для Управление файлами J avaScгipt Управление таблицами стилей CSS Работа с якорными элементами Работа с элементами img Использование кеша данных Установка времени истечения для кеша Использование вариаций кеша Использование URL, относительных к приложению Резюме Глава 755 756 757 759 759 760 762 763 765 768 770 771 772 776 778 778 26. Привязка моделей Подготовка проекта для примера Создание модели и хранилища Создание контроллера и представления Конфигурирование приложения Понятие привязки моделей Стандартные значения привязки Привязка простых типов JavaScrtpt и CSS 779 780 781 782 782 792 795 797 798 801 803 805 808 809 81 О 81 О 812 813 814 816 818 16 Содержание Привязка сложных типов Привязка массивов и коллекций Привязка коллекций сложных типов Указание источника данных привязки моделей Выбор стандартного источника данных привязки Использование заголовков в качестве источников данных привязки Использование тел запросов в качестве источников данных привязки Резюме Глава 27. Проверка достоверности моделей Подготовка проекта для примера Создание модели Создание контроллера Создание компоновки и представлений Необходимость в проверке достоверности модели Явная проверка достоверности модели Отображение пользователю ошибок проверки достоверности Отображение сообщений об ошибках проверки достоверности Отображение сообщений об ошибках проверки достоверности на уровне свойств Отображение сообщений об ошибках проверки достоверности на уровне модели Указание правил проверки достоверности с помощью метаданных Создание специального атрибута проверки достоверности для свойства Выполнение проверки достоверности на стороне клиента Выполнение удаленной проверки достоверности Резюме Глава 28. Введение в ASP.NET Core ldentity Подготовка проекта для примера Создание контроллера и представления Настройка ASP.NEТ Core Ideпtity Создание класса пользователя Создание класса контекста базы данных Конфигурирование настройки строки подключения к базе данных Создание базы данных Ideпtity Использование ASP.NEТ Core Identity Перечисление пользовательских учетных записей Создание пользователей Проверка паролей Проверка деталей, связанных с пользователем Завершение построения средств администрирования Реализация возможности удаления Реализация возможности редактирования Резюме Глава 29. Применение ASP. NET Core ldentity Подготовка проекта для примера Аутентификация пользователей Подготовка к реализации аутентификации Добавление аутентификации пользователей Тестирование аутентификации 819 829 832 835 837 838 841 843 844 844 846 846 84 7 850 850 853 855 860 861 864 867 870 873 876 877 879 880 882 882 883 884 886 886 887 889 893 900 905 906 907 912 913 913 914 916 919 922 Содержание Авторизация пользователей с помощью ролей Создание и удаление ролей Управление членством в ролях Использование ролей для авторизации Помещение в базу данных начальной информации Резюме Глава 30. Расширенные средства ASP.NET Core ldentity Подготовка проекта для примера Добавление специальных свойств в класс пользователя Подготовка миграции базы данных Тестирование специальных свойств Работа с заявками и политиками Понятие заявок Создание заявок Использование политик Использование политик для авторизации доступа к ресурсам Использование сторонней аутентификации Регистрация приложения в Google Google Включение аутентификации Резюме Глава 31. Соrлашения по модели и оrраничения действий 17 922 923 928 933 937 940 941 942 942 946 947 947 948 952 955 962 967 967 968 972 973 Резюме 973 974 976 977 981 982 987 988 989 990 992 993 998 1000 Предметный указатель 1001 Подготовка проекта для примера Создание модели представления. контроллера и представления Использование модели приложения и соглашений по модели Модель приложения Роль соглашений по модели Создание соглашения по модели Порядок применения соглашений по модели Создание глобальных соглашений по модели Использование ограничений действий Подготовка проекта для примера Ограничения действий Создание ограничения действия Распознавание зависимостей в ограничениях действий Посвящается моей прекрасной жене Джеки Гриффитс. Об авторе Адам Фримев - опытный специалист в области информационных технологий, занимавший ведущие позиции во многих компаниях, последней из которых был гло­ бальный банк, где он работал на должностях директора по внедрению технологий и руководителя административной службы. После ухода из банка Адам уделяет все свое время писательской деятельности и бегу на длинные дистанции. О техническом рецензенте Фабио КJiаудво Ферраччати - ведущий консультант и главный аналитик/раз­ BluArancio Microsoft разработчи­ ком решений для .NЕТ (Microsoft Certified Solution Developer for .NЕТ). сертифициро­ ванным Microsoft разработчиком приложений для .NЕТ (Microsoft Certified Application Developer for .NET), сертифицированным Microsoft профессионалом (Microsoft Certified Professional), а также продуктивным автором и техническим рецензентом. За пос­ работчик, использующий технологии (www .Ыuarancio. com). Microsoft. Он работает в компании Фабио является сертифицированным ледние десять лет он написал множество статей для итальянских и международных журналов и выступал в качестве соавтора в более чем темам, связанным с компьютерами. 1О книгах по разнообразным Ждем ваших отзывов! Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хо­ тим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бу­ мажное или электронное письмо либо просто посетить наш веб-сайт и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится ли вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Отправляя письмо или сообщение, не забудьте указать название книги и ее авто­ ров, а также свой обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию новых книг. Наши электронные адреса: E-mail: [email protected] WWW: http://www.dialektika.com ЧАСТЬ 1 Введение в инфраструктуру ASP.NET Core МVС 2 разработчиков веб-приложений, использующих платформу Microsoft, д ля инфраструктура ASP.NET Core MVC представляется как радикальное изме­ нение. В ней особое значение придается чистой архитектуре, паттернам проек­ тирования и удобству тестирования, а также не предпринимается попытка скры­ вать, каким образом работает веб-среда. Первая часть книги поможет понять в общих чертах основополагающие идеи разработки приложений MVC, включая новые возможности увидеть на деле, как применяется инфраструктура. ASP.NET Core MVC, и ГЛАВА 1 Основы ASP.NEТ A SP.NET Саге изводства MVC Microsoft. Core МVС это инфраструктура для разработки веб-приложений про­ которая сочетает в себе эффективность и аккуратность ар­ (mode\-view-controller - MVC). идеи .NET. В настоящей Microsoft создала инфраструктуру ASP.NET Соге хитектуры "модель-представление-контроллер" и приемы гибкой разработки. а также лучшие части платформы главе вы узнаете. почему компания MVC, увидите, как она соотносится со своими предшественниками и альтернативами. а также ознакомитесь с обзором нововведений ASP.NET Саге MVC и с тем . что будет рассматриваться в книге. История развития ASP.NET Core MVC ASP.NET появилась в 2002 году . в то время. Microsoft стремилась сохранить господствующее положение в области Первоначальная версия платформы когда компания разработки традиционных настольных приложений и видела в Интернете опасность. На рис . 1. 1 показано. как выглядел на то время стек технологий Microsoft. ASP.NETWeb Forms Набор компонентов пользовательского интерфейса (страниц, кнопок и т.д.) плюс объектно-ориентированная программная модель графического пользовательско го интерфейса с сохранением состояния ASP. N EТ Способ размещения приложений . NЕТ в llS (веб-серверный продукт Microsoft), позволяющий взаимодействовать с запросами и ответами НТТР .NET - Многоязычная платформа управляемого кода (совершенно новая на то время и по праву ставшая поворотным пунктом в развитии) Рис. 1.1. Стек технологий ASPNET Web Forms Глава 1. Основы ASP.NET Соге MVC 23 ASP.NET Web Forms В инфраструктуре Web Fonns разработчики из Microsoft пытались скрыть и прото­ HTML (которым на тот мо­ кол НТГР (с присущим ему отсутствием состояния), и язык мент не владели многие разработчики) за счет моделирования пользовательского ин­ терфейса как иерархии объектов, представляющих серверные элементы управления. Каждый элемент управления отслеживал собственное состояние между запросами, по мере необходимости визуализируя себя в виде НТМL-разметки и автоматически соединяя события клиентской стороны (например, щелчки на кнопках) с соответс­ твующим кодом их обработки на стороне сервера. Фактически Web Fonns является гигантским уровнем абстракции, предназначенным для доставки классического уп­ равляемого событиями графического пользовательского интерфейса через веб-среду. Идея заключалась в том, чтобы разработка веб-приложений выглядела почти так же, как разработка настольных приложений. Разработчики могли оперировать поня­ тиями пользовательского интерфейса, запоминающего состояние, и не иметь дело с последовательностями независимых запросов и ответов НТГР. У компании Microsoft появилась возможность плавно переместить армию разработчиков настольных Windоws-приложений в новый мир веб-приложений. Что было не так с ASP.NET Web Forms? Разработка с использованием традиционной технологии ASP.NEТ Web Forms в при­ нципе была хорошей, но реальность оказалась более сложной. • Тяжеловесность состояния представления (View State). Действительный меха­ низм поддержки состояния между запросами, известный как View State, вызвал необходимость передачи крупных блоков данных между клиентом и сервером. Объем таких данных мог достигать сотен килобайтов даже в скромных веб-при­ ложениях. Эти данные путешествуют туда и обратно с каждым запросом, при­ водя к замедлению реакции и увеличивая требование к ширине полосы пропус­ кания сервера. • Жизненный цикл страницы. Механизм соединения событий клиентской сто­ роны с кодом обработчиков событий на стороне сервера, являющийся частью жизненного цикла страницы, мог быть сложным и хрупким. Лишь немногим разработчикам удавалось успешно манипулировать иерархией элементов уп­ равления во время выполнения, не получая ошибки состояния представления или не сталкиваясь с ситуацией, когда некоторые обработчики событий зага­ дочным образом отказывались запускаться. • Ложное ощущение разделения обязанностей. Модель отделенного кода ASP.NET Web Forms предоставляла способ вынесения кода приложения из НТМL-разметки в специальный файл отделенного кода. Это было сделано с целью разделения логики и представления, но в действительности попустительствовало смеши­ ванию кода представления (например, манипуляций деревом элементов уп­ равления серверной стороны) с прикладной логикой (скажем, обработкой ин­ формации из базы данных) в гигантских классах отделенного кода. Конечный результат мог оказаться хрупким и непонятным. • Ограниченный контроль над НТМL-разметкой. Серверные элементы управле­ ния визуализировали себя в виде НТМL-разметки, но она не обязательно была такой, как хотелось. В ранних версиях Web Fonns выходная НТМL-разметка не Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 24 соответствовала веб-стандартам или неэффективно применяла каскадные таб­ лицы стилей (Cascading Style Sheets - CSS), а серверные элементы управления генерировали непредсказуемые и сложные атрибуты идентификаторов, к кото­ рым было трудно получать доступ с помощью JavaScript. Указанные проблемы Web Forms, но получение были значительно смягчены в последних выпусках ожидаемой НТМL-разметки по-прежнему могло быть затруднительным. Негерметичная абстракция. Инфраструктура • Web Forms, где только возможно, пыталась скрывать детали, связанные с НТМL и НТГР. При попытке реализовать специальное поведение абстракция часто отбрасывалась, поэтому Д11Я генериро­ вания желаемой НТМL-разметки приходилось воссоздавать механизм событий обратной отправки или предпринимать другие сложные действия. Низкая тестируемость. Проектировщики • Web Forms даже не предполагали, что автоматизированное тестирование станет неотъемлемой частью процесса раз­ работки программного обеспечения. Построенная ими тесно связанная архи­ тектура не бьша приспособлена ДllЯ модульного тестирования. Интеграционное тестирование также могло превратиться в сложную задачу. С технологией Web Forms не все бьшо плохо, и со временем в Microsoft приложили немало усилий по улучшению степени соответствия стандартам, упрощению процес­ са разработки и даже заимствованию ряда возможностей из первоначальной инфра­ структуры ASP.NEТ МVС Forms Framework ДllЯ их применения в Web Forms. Технология Web превосходна, когда необходимы быстрые результаты, и с ее помощью вполне реально построить и запустить веб-приложение умеренной сложности всего за один день. Но если не проявлять осторожность во время разработки, то обнаружится, что созданное приложение трудно тестировать и сопровождать. Первоначальная инфраструктура МУС В октябре 2007 года компания Framework Microsoft объявила о выходе новой платформы раз­ работки, построенной на основе существующей платформы ASP.NEТ, которая бьша задумана как прямая реакция на критику платформ наподобие MVC Framework Ruby оп Rails. Web Forms и популярность конкурирующих Новая платформа получила название ASP.NET и отражала формирующиеся тенденции в разработке веб-приложе­ ний, такие как стандартизация HTML и CSS, веб-службы REST, эффективное модуль­ ное тестирование. а также идея о том, что разработчики должны принять факт от­ сутствия состояния у НТГР. Концепции, которые подкрепляли первоначальную MVC 2007 году мир разра­ ботки веб-приложений .NЕТ бьш их лишен. Появление ASP.NEТ MVC Framework воз­ вратило платформу разработки веб-приложений Microsoft в современную эпоху. Инфраструктура МVС Framework также сигнализировала о важном изменении в отношении компании Microsoft, которая ранее пыталась контролировать каждый ком­ Framework, инфраструктуру теперь кажутся естественными и очевидными, но в понент в инструментальных средствах, необходимых ДllЯ веб-приложений. Компания Microsoft встроила в инфраструктуру МVС дом, такие как jQuery. учла проектные Framework инструменты с открытым ко­ соглашения и передовой опыт конкурирующих (и более успешных) платформ. а также выпустила в свет исходный код МVС ДllЯ изучения разработчиками. Framework Глава 1. Основы ASP.NET Core MVC Что было не так с первоначальной инфраструктурой На то время для Microsoft имело 25 MVC Framework? смысл создавать инфраструктуру МVС Framework поверх существующей платформы, содержащей большой объем монолитной низ­ коуровневой функциональности, которая обеспечила преимущество в начале про­ цесса разработки и которую хорошо знали и понимали разработчики приложений ASP.NET. MVC Framework на платформе, которая из­ Web Forms. Разработчики MVC Framework привыкли Но компромиссы потребовали основать начально предназначалась для использовать конфиrурационные параметры и кодовые настройки, отключающие или реконфиrурирующие средства, которые не имели ни малейшего отношения к их веб­ приложениям, но требовались для того, чтобы все заработало. С ростом популярности MVC Framework компания Microsoft приступила к добав­ Web Forms. Результат был все более и более лению ряда ее основных возможностей к странным, когда индивидуальные особенности проекта, требуемые для поддержки MVC Framework, расширялись с целью поддержки Web Forms, и предпринимались до­ полнительные проектные ухищрения, чтобы подогнать все друг к друrу. Одновременно в Microsoft начали расширять ASP.NET новыми (Web API) и организации коммуникаций служб инфраструктурами для создания веб­ в реальном времени (SignalR). Новые инфраструктуры добавили собственные соглашения по конфигурированию и разра­ ботке, каждое из которых обладало своими преимуществами и причудами, породив в результате фрагментированную смесь. Обзор В ASP.NET Core 2015 году Microsoft заявила о новом направлении для ASP.NEТ и МVС Framework, Core МVС, рассматривае­ что в итоге привело к появлению инфраструктуры ASP.NEТ мой в настоящей книге. .NET Core, которая представляет Framework без интерфейсов программирова­ ния приложений (applicatioп programming interface - API), специфичных для Windows. Тhсподствующей операционной системой по-прежнему является Windows, но веб-при­ Платформа ASP.NEТ Core построена на основе собой межплатформенную версию .NЕТ ложения все чаще размещаются в небольших и простых контейнерах на облачных платформах. За счет принятия межплатформенного подхода компания ширила область охвата Core .NET, Microsoft рас­ ASP.NET сделав возможным развертывание приложений на более широком наборе сред размещения, а в качестве бонуса предоставила разработчикам возможность создавать веб-приложения macOS. ASP.NEТ Core - Linux ASP.NET Core на машинах и совершенно новая инфраструктура. Она проще, с нею легче рабо­ тать, и она свободна от наследия, происходящего из на .NЕТ Core, Web Forms. Будучи основанной она поддерживает разработку веб-приложений для ряда платформ и контейнеров. Инфраструктура ASP.NET Core MVC предлагает функциональность первоначаль­ Framework, построенной поверх новой платформы ной инфраструктуры ASP.NEТ МVС ASP.NET Core. Она интегрирует лялись Web API, поддерживает функциональные средства, которые ранее предостав­ более естественный способ генерирования сложного содержимого и делает основные задачи разработки, такие как модульное тестирова­ ние. более простыми и предсказуемыми. 26 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Что нового в Выпуск ASP.NET Core MVC 2? ASP.NET Core MVC 2 концентрируется на консолидации за счет того, что задействует в своих интересах изменения в инструментах и платформе, которые были введены в более ранних версиях. Для работы ся платформа .NET Core 2, ASP.NET Core MVC 2 требует­ которая имеет значительно расширенное покрытие АРI­ интерфейсами и теперь поддерживается дополнительными дистрибутивами Linux. Полезные изменения включают новую систему мета-пакетов, упрощающую управле­ NuGet. новую систему конфигурации для ASP.NET Core и поддержку для Entity Framework Core 2. Крупнейшим новым средством стала инфраструктура Razor Pages, которая представляет собой попытку воссоздания стиля разработки. ассоци­ ированного с инфраструктурой Web Pages, с применением более современной плат­ формы, но Razor Pages не будет интересна разработчикам МVС (и в данной книге не ние пакетами рассматривается). Основные преимущества ASP.NET Core MVC В последующих разделах кратко описано, чем новая платформа унаследованную инфраструктуру Framework, Web Forms и что именно позволит снова вывести Архитектура MVC превосходит и первоначальную инфраструктуру ASP.NET на MVC передний край. MVC Инфраструктура ASP.NET Core MVC следует паттерну под названием "модель­ (model-view-controller - МVС), который управляет фор­ веб-приложения ASP.NET и взаимодействиями между содержащимися в нем предсmавление-конmроллер" мой компонентами. Важно различать архитектурный паттерн MVC и реализацию ASP.NET Core MVC. MVC далеко не нов (его появление датируется 1978 годом и связано с про­ Smalltalk в Xerox PARC), но в наши дни он завоевал популярность в качестве Паттерн ектом паттерна для веб-приложений по перечисленным ниже причинам. • Взаимодействие пользователя с приложением, которое придерживается паттер­ ном MVC, следует естественному циклу: пользователь предпринимает действие, а приложение в ответ изменяет свою модель данных и доставляет обновленное представление пользователю. Затем цикл повторяется. Это удобно укладывает­ ся в схему веб-приложений, предоставляемых в виде последовательностей за­ просов и ответов НТТР. • Неб-приложения, нуждающиеся в сочетании нескольких технологий (например, баз данных, НТМL-разметки и исполняемого кода), обычно разделяются на на­ бор слоев или уровней. Полученные в результате таких сочетаний комбинации естественным образом отображаются на концепции в паттерне МVС. Инфраструктура ASP.NET Core МVС реализует паттерн МVС и вдобавок обеспечи­ вает гораздо лучшее разделение обязанностей по сравнению с деле в ASP.NET Core MVC внедрена разновидность паттерна Web Forms. На самом MVC, которая особенно хорошо подходит для веб-приложений. Дополнительные сведения по теории и прак­ тике применения такой архитектуры приведены в главе 3. Глава 1. Основы ASP.NET Соге MVC 27 Расширяемость Инфраструктуры ASP.NET Core и ASP.NET Core MVC построены в виде последова­ тельности независимых компонентов, которые имеют четко определенные характе­ ристики, удовлетворяют интерфейсу .NЕТ или созданы на основе абстрактного базо­ вого класса. Основные компоненты можно легко заменять другими компонентами с собственной реализацией. В общем случае для каждого компонента инфраструктура ASP.NET Core MVC предлагает три возможности. Использование стандартной реализации компонента в том виде, как есть (чего • должно быть достаточно для большинства приложений). Создание подкласса стандартной реализации с целью корректировки существу­ • ющего поведения. Полная замена компонента новой реализацией интерфейса или абстрактного • базового класса. Разнообразные компоненты, а также способы и причины их возможной настройки или замены будут рассматриваться, начиная с главы Жесткий контроль над Инфраструктура HTML 14. и НТТР ASP.NET Core MVC генерирует ясную и соответствующую стан­ дартам разметку. Ее встроенные вспомогательные функции дескрипторов (tag helper) Web Foпns име­ производят соответствующий стандартам вывод, но по сравнению с ется более значимое философское изменение. Вместо генерации громадного объема НТМL-разметки, над которой вы имеете очень небольшой контроль, инфраструктура ASP.NEТ Core с помощью МVС поощряет создание простой и элегантной разметки, стилизованной css. Конечно, если вы действительно хотите добавить готовые виджеты для таких слож­ ных элементов пользовательского интерфейса, как окна выбора даты или каскадные ASP.NET Core МVС, jQuery, ASP.NET Core MVC настолько тес­ меню, то подход "никаких специальных требований", принятый в позволяет легко использовать наилучшие клиентские библиотеки, подобные Aпgular, React или Bootstrap CSS. Инфраструктура но сплетена с этими библиотеками, что в Microsoft предусмотрели шаблоны, которые включают их, чтобы придать ускорение процессу разработки новых проектов. Инфраструктура ASP.NET Core MVC работает в гармонии с НТТР. Вы имеете кон­ троль над запросами, передаваемыми между браузером и сервером, так что можете точно настраивать пользовательский интерфейс по своему усмотрению. Технология Ajax сделана легкой в применении, и создание веб-служб для получения браузерных НТТР-запросов выливается в простой процесс, который описан в главе 20. Тестируемость Естественное разнесение различных обязанностей приложения по независимым друг от друга частям, которое поддерживается архитектурой ASP.NET Core МVС, поз­ воляет с самого начала делать приложение легко сопровождаемым и удобным для тестирования. Вдобавок каждый фрагмент платформы туры ASP.NET Core MVC ASP.NET Core и инфраструк­ может быть изолирован и заменен в целях модульного тес­ тирования, которое допускается выполнять с использованием любой популярной ин­ фраструктуры тестирования с открытым кодом, такой как главе 7. xUnit, рассматриваемой в 28 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 В настоящей книге вы увидите примеры написания ясных и простых модульных тестов для контроллеров и действий MVC, которые предоставляют фиктивные либо имитированные реализации компонентов инфраструктуры для эмуляции любого сце­ нария с применением разнообразных стратегий тестирования и имитации. Даже если вам никогда ранее не приходилось создавать модульные тесты, у вас будет все необ­ ходимое для успешного старта. Тестируемость Приложения имеет отношение ASP.NET Core MVC успешно не только к модульному тестированию. работают также с инструментами тестирова­ ния, встроенными в средства автоматизации пользовательского интерфейса. Можно создавать тестовые сценарии, которые имитируют взаимодействие с пользователем, не выдвигая догадки о том, какие структуры НТМL-элементов, классы CSS или иден­ тификаторы сгенерирует инфраструктура. и не испытывая беспокойства по поводу неожиданных изменений структуры. Мощная система маршрутизации По мере совершенствования технологии построения веб-приложений эволюциони­ ровал стиль унифицированных указателей ресурсов Адреса URL /Арр (uniform resource locator - URL). вроде приведенного ниже: v2/User/Page.aspx?action=show%20prop&prop id=82742 встречаются все реже, а им на смену приходит более простой и понятный формат следующего вида: /to-rent/chicago/2303-silver-street Существует ряд веских причин для того, чтобы заботиться о структуре URL- aдpecoв. Во-первых. поисковые механизмы придают вес ключевым словам, содер­ жащимся в URL. Поиск по фразе "reпt in Chicago" ("аренда в Чикаго") с большей URL. Во-вторых, многие веб-пользователи понимать URL, и ценят возможность навигации вероятностью обнаружит более простой достаточно сообразительны. чтобы путем ввода запроса в адресной строке своего браузера. В-третьих. когда структура URL-aдpeca понятна, люди с большей вероятностью пройдут по нему. поделятся им с другими или даже продиктуют его по телефону. В-четвертых. при таком подходе в Интернете не раскрываются технические детали. структура каталогов и имен файлов приложения; следовательно, вы вольны изменять лежащую в основе сайта реализа­ цию. не нарушая работоспособности всех входящих ссылок. В более ранних инфраструктурах понятные URL-aдpeca реализовать было трудно, ASP.NET Core но в МVС используется средство, известное как маршрутизация URL. ко­ торое обеспечивает предоставление понятных URL-aдpecoв по умолчанию. В результате появляется контроль над схемой URL и ее взаимосвязью с приложением, что обеспечи­ вает свободу создания понятного и удобного для пользователей шаблона URL без необ­ ходимости следования какому-то заранее определенному шаблону. И. разумеется, это URL в стиле REST, если URL приведено в главах 15 и 16. означает также простоту определения современной схемы нужна. Подробное описание маршрутизации она Современный АРl-интерфейс Платформа вая - ния. Microsoft .NET развивалась с каждым крупным выпуском, поддержи­ - многие передовые аспекты современного программирова­ Инфраструктура ASP.NET Core MVC построена для платформы .NET Core, поэтои даже определяя Глава 1. Основы ASP.NET Core MVC 29 му ее АРI-интерфейс может в полной мере задействовать последние новшества языка и исполняющей среды, знакомые программистам на С#, в том числе ключевое слово awai t, расширяющие методы, лямбда-выражения, анонимные и динамические типы, а также язык интегрированных запросов (Language lntegrated Query - LINQ). ASP.NET Core MVC Многие методы и паттерны кодирования АРI-интерфейса следу­ ют более четкой и выразительной композиции, чем было возможно в ранних версиях инфраструктуры. Не переживайте, если вы пока не в курсе последних функциональ­ ных возможностей языка С#: в главе твам С# для разработки приложений 4 представлена сводка по самым важным средс­ MVC. Межплатформенная природа ASP.NET были специфичными для Windows, требуя на­ Windows при написании веб-приложений и сервер Windows и выполнении. Компания Microsoft сделала инфраструктуру Предшествующие версии стольный компьютер с при их развертывании ASP.NET Core межплатформенной, как в отношении разработки, так и в плане раз­ macOS Linux. Межплатформенная поддержка облегчает развертывание приложений ASP.NET Core MVC, к тому же имеется хорошая подде­ ржка работы с платформами контейнеров приложений, такими как Docker. Вполне вероятно, большая часть разработки приложений ASP.NET Core MVC в ближайшем будущем будет выполняться с применением Visual Studio, но компания Microsoft также создала межплатформенный инструмент разработки под названием Visual Studio Code, появление которого означает, что разработка ASP.NET Core МVС больше не ограничена средой Windows. вертывания. Продукт .NET Core доступен для различных платформ, включая и набор популярных дистрибутивов Инфраструктура ASP.NEТ Core MVC имеет открытый код В отличие от предшествующих платформ для разработки веб-приложений произ­ водства Microsoft вы можете загрузить исходный код ASP.NET Core и ASP.NET Core МVС и даже модифицировать и компилировать его с целью получения собственных версий инфраструктур. Это бесценно при отладке кода, обращающегося к системно­ му компоненту, когда требуется пошагово выполнить его код (и даже почитать ори­ гинальные комментарии программистов). Это также полезно, если вы создаете усо­ вершенствованный компонент и хотите посмотреть, какие существуют возможности разработки, или узнать, как действительно работают встроенные компоненты. Исходный код и ASP.NET Core ASP.NET Core MVC доступен для загрузки по адресу https://github.com/aspnet. Что необходимо знать? Чтобы получить от книги максимальную пользу, вы должны быть знакомы с осно­ вами разработки веб-приложений, понимать HTML и CSS, а также иметь практичес­ кий опыт работы с языком С#. Не беспокойтесь, если детали разработки клиентской стороны, такие как JavaScript, для вас несколько туманны. Основной акцент в книге делается на разработке серверной стороны, и благодаря примерам вы сможете подоб­ сводка по наиболее полезным средствам рать то, что вам нужно. В главе 4 приводится языка С# для разработки которую вы сочтете удобной, если переходите на пос­ ледние версии .NET с MVC, более раннего выпуска. Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 30 Какова структура книги? Книга разделена на две части, в каждой из которых раскрывается набор связан­ ных тем. Часть 1. Введение в инфраструктуру Книга начинается с помещения ASP.NET Core MVC ASP.NET Core МVС в контекст разработки. Здесь объяснены преимущества и практическое влияние паттерна МVС, рассмотрен способ, которым инфраструктура ASP.NET Core МVС вписывается в современную разработку веб-приложений, а также описаны инструменты и средства языка С#, необходимые каждому программисту В главе 2 ASP.NET Core MVC. мы углубимся в детали и создадим простое веб-приложение, чтобы по­ лучить представление о том, каковы основные компоненты и строительные блоки, и каким образом они сочетаются друг с другом. Однако большинство материала этой части посвящено разработке проекта под названием SportsStore. посредством которо­ го демонстрируется реалистичный процесс разработки, начиная с постановки задачи и заканчивая развертыванием, с привлечением основных функциональных возмож­ ностей Часть ASP.NET Core 11. МVС. Подробные сведения об инфраструктуре ASP.NET Core MVC ASP.NET Core МVС, ко­ SportsStore. Будет показано, как Во второй части объясняется внутренняя работа средств торые использовались при построении приложения работает каждое средство, объяснена его роль и описаны доступные варианты конфи­ гурирования и настройки. Широкий контекст, представленный в первой части, под­ робно раскрывается во второй части. Где можно получить код примеров? Все примеры, рассмотренные в книге, доступны для загрузки по адресу gi thub. com/apress/pro-asp. net-core-mvc-2 https: / / и на веб-сайте издательства. Загру­ жаемый файл доступен бесплатно и включает все поддерживающие ресурсы, кото­ рые требуются для воссоздания примеров без необходимости в их ручном наборе. Загружать код необязательно, но это самый простой путь для экспериментирования с примерами и способ использования фрагментов кода в собственных проектах. Резюме В главе был объяснен контекст, в котором существует инфраструктура ASP.NEТ Web Forms и первоначальной инфраструк­ ASP.NET MVC Framework. Кроме того, рассматривались преимущества приме­ нения ASP.NET Core MVC и структура книги. В следующей главе вы увидите инфра­ структуру ASP.NET Core МVС в действии благодаря простой демонстрации средств, Core МVС, а также описано ее развитие от туры которые обеспечивают все ее преимущества. ГЛАВА 2 Ваше первое приложение МVС л учший способ оценки инфраструктуры. предназначенной для разработки про­ граммного обеспечения. заключается в том, чтобы приступить непосредствен­ но к ее использованию. В настоящей главе мы создадим простое приложение ввода данных с применением ASP.NET Соге MVC. Мы будем решать эту задачу пошагово, чтобы вы поняли, каким образом строится приложение МVС. Для простоты мы пока опускаем некоторые технические подробности. Но не беспокойтесь - если вы только начинаете знакомство с МVС, то узнаете много интересного. Когда что-либо исполь­ зуется без пояснения, то приводится ссылка на главу, в которой находятся все необ­ ходимые детали. Установка Visual Studio Настоящая книга опирается на продукт ет среду разработки для проектов Visual Studio 2017, который предоставля­ ASP.NET Core MVC. Мы будем применять бесплат­ ную редакцию Visual Studio 2017 Community, доступную для загрузки на веб-сайте www. visualstudio. сот. При установке Visual Studio 2017 вы должны выбрать ра­ бочую нагрузку .NET Core cross-platform development (Межплатформенная разработка .NET Core). как показано на рис. 2.1. На заметку! Версия чае установки Visual Studio 2017 предшествовала выпуску ASP.NET Core MVC 2. В слу­ Visual Studio для более ранних версий ASP.NET Core MVC понадобится при­ менить последние обновления. Применить обновления можно, запустив программу уста­ новки Visual Studio и щелкнув на кнопке Update (Обновить). Совет. Среда Visual Studio поддерживает только Windows. Создавать приложения ASP.NET Core MVC на других платформах можно с использованием Visual Studio Code. Продукт Visual Studio Code не обладает всеми возможностями Visual Studio, но он предлагает ве­ ликолепный редактор и любые средства, требующиеся для разработки приложений MVC. Детали ищите в главе 13. 32 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 1::1 х - Modifylng-V>Sual Stud10 Commun1ty 2017 - Workloads х 15.3 О lndividual components language packs Summary Other Toolsets (3) > Visual Studio core editor Vlsual Studlo extension development Create add·.oм and tr~ns1~ns •or V:sual Stud10 1ncluding ntW commal3ds, codt .naly:"ers and tool w1ndows. v .NET Core cross-platform development 1ncluded .,, .NEТCore 1.0- 1.1 developmenttock: Unux d~etopment with С ... • .,, .NЕТ .J ASP.NH ind wеЬ di'Velopment tools Framewortc 4.6. 1 d~elopmft'\t tools .,, De'IQfoper Anatyt1cs tools Opt1onal ~ Contau-,er dtv2lopm~nt tools L!.11 Cloud Е.<рlош .NП Cort c1oss·pLatfor·m deve\opment Зutld cross·platform •ppltc•t1ons usin9 .NET Core ASPJ..fТ Cort , НТМ!., Ja...1Scnp ьnd c.ontьintt dt"elopment tools.. L!.11 .NЕТ prof11in9 tools LOcatIOn ,.. ·"" ( \f1 f'j Total 1nstaH s1ze: Ву continuing. you egrt-t t0 the lic~ for the Visual Stud.IO ~ition yot.t s.d«.tIO. We also offer ~ 1Ь.1 1tу to dO'tonJo&d cthl!r so~ with VtSu1I StudIO. Тhts softwart is Jкmse<f я-p..v1ttly. ..s ш out 111 tМ 3rd Party tilooc~ or in rt5 .J(Cornpan)'lng l1c~e. 8у cont1nuing уоо .Jlso ьgrt-e to tho~ bc~s. Рис. Установка 2. 1. Выбор рабочей нагрузки О kB Mod1fy Visual Studio .NET Core 2.0 SDK Установка ложений Visual Studio содержит все средства. необходимые для разработки при­ ASP.NET Core MVC. но не включает комплект .NET Core 2.0 SDK. который должен быть загружен и установлен отдельно. Перейдем по ссылке https: / / www. microsoft.com/ net / core. загрузим програм­ SDK для Windows и запустим ее. После завершения работы программы установки откроем окно командной строки или окно PowerShell и введем следующую команду для отображения установленной версии платформы .NET: му установки .NET Соге dotnet -- versi on Если установка прошла успешно. тогда результирующим выводом команды будет 2. о. о. Создание нового проекта ASP.NET Core MVC Мы собираемся начать с создания нового проекта Studio. ASP.NET Core MVC в среде Visual File (Файл) пункт NewqProject (СоздатьqПроект) . чтобы от ­ New Project (Новый проект) . Перейдя в раздел TemplatesqVisual Выберем в меню крыть диалоговое окно C#qWeb (WaблoныqVisual С#9Веб) в панели слева, можно заметить шаблон проекта ASP.NET Core Web Application (Веб-приложение ASP.NET Core). Выберем этот тип про­ екта (рис. 2.2). Глава 2. Ваше nервое nриложение MVC ~ ·! П' [@ "' f .NEТfr•mework~ SortЬy.@<f!U'it i> Recent ~ lмtolled ....~ ASP.NH CoreWeb ~р[ц:оtюn vtsщl (# Туре: 33 Sшch(Ctrl•E) Р· V'isu•I (: "~=====-----------.;...i Proje:cttempl1tesforcreatin9 ASP. NП Corrapplications for Windows, Linux and ASP .NEТ Yleb Applic•lюn (.NЕТ Frame.. Visual (" Vtsual (;: Windows Cfassic D~ktop macOS us1n9 w.ь .NП Core or .NЕТ Fr1mework. .NE'!Core е> . NП St•nd•rd Cloud Test Visu.1l81sic <;()! ,,_,,., Not find1ng wh1t you .are toolang fot? Open Vrsual Studio lnstoller Name: location: Sotution mme: P•rtylnvites @u.e~~ ----------~-:::=3 P•rtylnvites 0 Brows". Crtate directory f or solution О ,Add to Sourct Control Г Рис. 2.2. Выбор шаблона nроекта _o!:._::J C•ncel ASP.NET Core Web Application Совет. При выборе шаблона проекта может возникать путаница из-за сильно похожих на­ званий шаблонов. Шаблон ASP.NET Web Application (.NET Framework) (Веб-приложение ASP.NET (.NET Framework)) предназначен для создания проектов с использованием унас­ ледованных версий ASP.NET и MVC Framework, которые предшествовали ASP.NET Core. Указанные два шаблона позволяют создавать приложения ASP.NET Core и отличаются применяемой исполняющей средой, предлагая на выбор .NET Framework или .NET Core. Разница между ними объясняется в главе 6, но в книге повсеместно используется вари­ ант .NET Core, поэтому для получения идентичных результатов при работе с примерами приложений вы должны выбирать именно его. В поле Name (Имя) для нового проекта введем Partyinv ites. Для продолжения щелкнем на кнопке ОК. Откроется еще одно диалоговое окно. которое предложит установить начальное содержимое проекта. Удостоверимся, что в раскрывающихся списках слева вверху выбраны элементы Для шаблона .NET Core и ASP.NET Core 2.0 (рис. 2.3). ASP.NET Core Web Application доступно несколько вариантов, каждый из которых приводит к созданию проекта с отличающимся начальным содержимым. Для целей данной главы выберем вариант Web Application (Model-View-Controller) приложение (модель-представление-контроллер)). который создаст приложение (Веб­ MVC заранее определенным содержимым, чтобы немедленно приступить к разработке. На заметку! Шаблон проекта Web Application (Model-View-Controller) применяется только в настоящей главе. Мне не нравится пользоваться заранее определенными шаблонами , поскольку они потворствуют интерпретации ряда важных средств наподобие аутентифи­ кации как черных ящиков. Моя цель здесь - предоставить вам достаточный объем знаний для понимания и управления каждым аспектом приложения материалах книги будет применяться шаблон Empty MVC, так что в оставшихся (Пустой). Текущая глава посвящена быстрому началу процесса разработки , для чего хорошо подходит шаблон (Model-View-Controller) . Web Application с 34 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 А project tempf1te for Empty creating an ASP.NEТ Core •pplicillion with e><•mple ASP .NЕТ Core МVС Views •nd Controller>. This templ•t• can •lso bt used for RESПul НТТР services. Web API Wш..!!!ш React.js React.js •nd Redux Ch•nge Authentication Authenticгtion No Authentiation О En•ЬI• Docker Support OS: Windows Requires Docktr for Windom Docker support can •lso Ь• en•Ьled l•t•r ~ OIC Рис. 2.3. 11 C•nc•I Выбор начальной конфигурации проекта Щелкнем на кнопке Change Authentication (Изменить аутентификацию) и в открыв­ Change Authentication (Изменение аутентификации) проверим. что выбран переключатель No Authentication (Аутентификация отсутствует) , как пока­ зано на рис. 2.4. Данный проект не требует какой-либо аутентификации , а в главах 28- 30 будет объясняться, как защищать приложения ASP.NET. Щелкнем на кнопке ОК, чтобы закрыть диалоговое окно Change Authentication. Удостоверимся в том, что флажок ЕnаЫе Docker Support (Включить поддержку Docker) шемся диалоговом окне не отмечен, и затем щелкнем на кнопке ОК для создания проекта Change Autl1~nticat1on · • . For applications that don't require any user authentication. @ No Authenticotion О lndividual User Accounts О Work or School Accounts О Windows Authentication bel!m mgre obout thjrd-party op~n source autbenticotion options Рис. 2.4. Выбор настроек аутентификации Pa rtyinv i tes. Х Глава 2. Ваше первое приложение MVC После того как среда Visual Studio 35 создала проект, в окне (Проводник решения) появится множество файлов и папок (рис. Solution Explorer 2.5). Они представ­ ляют собой стандартную структуру для проекта MVC. созданного с использованием Web Application (Model-View-Controller), и вскоре вы узнаете назначение каж­ файла и папки, которые были созданы Visual Studio. шаблона дого Solution Explorer Р· G'> Connected Services 1> 1> 1> 1> 1> .~i' Dependencies Properties @ wwwroot Controllers Models JI Views 1> 1> 1> Рис. 2.5. 61 61 61 1> С" 1> С" appsettingsJson bower.json bundleconfig.json Program.cs Startup.cs Начальная структура файлов и папок проекта Совет. Если вместо папок Controllers, Models и Views ASP.NET Core MVC вы видите папку Pages, то это означает, что вы выбрали шаблон Web Apptication (Веб-nриложение), а не шаблон Web Apptication (Model-View-Controller). Я не имею ни малейшего понятия о том, почему в Microsoft решили, что настолько похожие названия шаблонов будут удачной идеей, но в подобной ситуации понадобится удалить созданный проект и начать все заново. Теперь приложение можно запустить, выбрав в меню Debugging Debug (Отладка) пункт нужно просто щелкнуть на кнопке ОК. Среда с помощью сервера приложений IIS Express Visual Studio скомпилирует приложение. запустит его и откроет окно веб-браузера для запроса содержимого приложения. При запуске проекта в первый раз среде Studio Start (Запустить отладку); если появится запрос на включение отладки, тогда Visual потребуется некоторое время; по завершении процесса будет получен резуль­ тат. показанный на рис. 2.6. Visual Studio создает проект с применением шаблона Web Apptication (Model-View-Controtter), она добавляет базовый код и содержимое, которое вы наблю­ Когда среда даете после запуска приложения. Далее в главе мы заменим такое содержимое, чтобы создать простое приложение MVC. По завершении понадобится остановить отладку. для чего закрыть окно брау ­ зера или вернуться в (Остановить отладку). Visual Studio и выбрать в меню Debug пункт Stop Debugging 36 Часть 1. Введение в инфраструктуру ASP NET Саге MVC 2 Application uses _,,,,,r Overview How to '*' °' Run & Deploy 11i • Concepfuo{ оvем w uf • Add а Controi;er and '\/tri1N • ~'l'JP'!!~s~t_,,-.ASP.NEТ t ; i . - J • Samplt pagu utmg ASP.NET * Рис. 2.6. • Runy0urapp .,.,~~~'""" Выполнение примера проекта Вы видели. что для отображения проекта среда Visual Studio открывает окно бра­ узера. Можно указать любой установленный браузер. щелкнув на кнопке со стрелкой llS Express в панели инструментов и выбрав нужный Web Browser (Веб-браузер) . как показано на рис. 2. 7. правее кнопки ка в меню Jert CodeMaid Anгlyze Window вариант из спис­ Help ~· i .P; 115 Expre:ss llS Express Partylnvites Web Browser (Google Chrome) Google Chrome Google Chrome Canary ~ lnternet Explorer Microsoft Edge Рис. 2.7. Выбор браузера В дальнейшем во всех примерах будет использоваться браузер Google Chrome Canary. Microsoft Edge. Google Chrome или но вы можете применять любой современный браузер . включая Глава 2. Ваше первое приложение MVC 37 Добавление контроллера В рамках паттерна В ASP.NET от класса Саге MVC MVC входящие запросы обрабатываются контроллерами. контроллеры - это просто классы С# (обычно унаследованные Microsoft .AspNetCore .Mvc. Controller, базовым классом контроллера MVC). который является встроенным Каждый открытый метод в контроллере назы­ вается методом действия, что означает возможность его вызова из веб-среды через URL для выполнения какого-то действия. В MVC контроллеры помещаются в папку Controllers, Visual Studio при настройке проекта. некоторый соответствии с соглашением автоматически создаваемую Совет. Вы вовсе не обязаны соблюдать указанное или большинство других соглашений MVC, но рекомендуется его придерживаться - и не в последнюю очередь потому, что это по­ может уяснить примеры, приведенные в настоящей книге. Среда Visual Studio добавляет в проект класс стандартного контроллера, который Controllers в окне Solutioп Explorer. Файл называет­ HomeController. cs. Файлы классов контроллеров имеют имена, завершающиеся словом Controller, т.е. в файле HomeController. cs содержится код контроллера по можно увидеть. раскрыв папку ся имени Ноте - стандартного контроллера. используемого в приложениях МVС. Щелкнем на имени файла HomeController. cs в окне Solution Explorer. чтобы среда Visual Studio 2.1. открыла его для редактирования. Отобразится код С#, приведенный в листинге Листинг 2.1. Первоначальное содержимое файла из папки using using using using using using using HomeController. cs Controllers System; System.Collections.Gener ic; System.Diagnostics; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Mvc ; Partyinvites.Models; namespace Partyinvites.Controllers puЫic class HomeController : Controller puЫic IActionResult Index() { return View () ; IActionResult About() { ViewData [ "Message") = "Your application description page. "; return View (); puЫic IActionResult Contact() { ViewData["Message"J = "Your contact page."; return View (); puЫic IActionResult Error() { return View(new ErrorViewModel { Requestid ?? HttpContext.Traceidentif ier )) ; puЫic Activity.Current?.Id 38 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Заменим код в файле HomeContr o ller. cs кодом. показанным в листинге 2 .2. Здесь были удалены все методы кроме одного, у которого изменен возвращаемый тип и его реализация, а также удалены операторы usi n g для неиспользуемых про­ странств имен. Листинг 2.2. Изменение содержимого файла из папки Homecontroller. cs Controllers using Microsoft.AspNetCo r e .Mvc; namespace Partylnvites.Controllers puЫic class HomeController : Con troller puЫic string Index () { return "Hell o World"; Изменения не приводят к особо впечатляющим результатам. но их вполне доста­ точно для хорошей демонстрации . Метод по имени он возвращает строку Debugging из меню "Hello World". Debug в Index () изменен так. что теперь Снова запустим проект, выбрав пункт Start Visua\ Studio. Совет. Если вы оставили в функционирующем состоянии приложение из предыдущего раз­ дела, тогда выберите в меню Stop Debuggiпg и затем Debug пункт Restart (Перезапустить) Start Debuggiпg. или при желании пункт Браузер сделает НТТР-запрос серверу. Стандартная конфигурация MVC матривает, что данный запрос будет обрабатываться с применением метода предус­ I ndex ( ), называемого методом действия или просто действием . а результат. полученный из этого метода . будет отправлен обратно браузеру (рис. 2.8). С 1 <D localhost:57628 Hello World Рис. 2.8. Совет. Обратите внимание, что среда URL, Вывод из метода действия Visual Studio направляет браузер на порт 57628. Внутри который будет запрашивать ваш браузер , почти наверняка будет присутствовать другой номер порта, т.к. Visual Studio при создании проекта выделяет произвольный порт. Если вы заглянете в область уведомлений панели задач Windows, то найдете там значок llS Express. Данный значок представляет усеченную версию полного сервера прило­ жений llS, которая входит в состав Visual Studio и используется для доставки содержимого и служб ASP.NEТ во время разработки . Развертывание проекта MVC в производственной среде будет описано в главе 12. для Глава 2. Ваше первое приложение MVC 39 Понятие маршрутов В дополнение к моделям, представлениям и контроллерам в приложениях применяется система маршрутизации ASP.NEТ, которая определяет, как ражаются на контроллеры и действия. Маршрут начальных. Можно запрашивать любой из следующих . • Visual Studio создает она добавляет ряд стандартных маршрутов, выступающих в качестве MVC, на действие MVC отоб­ это правило, которое использу­ - ется для решения о том, как обрабатывать запрос. Когда среда проект URL Index класса URL, и они будут направлены HomeController: / /Ноте • /Home/Index Таким образом, когда браузер запрашивает httр://ваш-сайт/ или ваш_ сайт/Ноте, он получает вывод из метода Index () Можете проверить сказанное самостоятельно, изменив момент он будет выглядеть как тат - строку <Enter>. URL в URL порцию http:// HomeController. браузере. В настоящий ht tp: / /localhost: 5 7 62 8 /,но часть может быть другой. Если вы добавите к и нажмете клавишу класса представляющая порт /Home или /Home/Index то получите от приложения МVС тот же самый резуль­ "Hello World". Это хороший пример получения выгоды от соблюдения соглашений. поддержива­ емых ASP.NET Core MVC. В данном случае соглашение заключается в том, что сущес­ твует класс контроллера по имени HomeController, который будет служить старто­ вой точкой приложения МVС. Стандартная конфигурация, создаваемая средой Studio для нового проекта, Visual предполагает, что мы будем следовать такому соглашению. И поскольку мы действительно соблюдаем соглашение, то автоматически получаем поддержку всех URL из приведенного выше списка. Если не следовать соглашению, тогда конфигурацию пришлось бы модифицировать для указания на контроллер, со­ зданный взамен стандартного. В рассматриваемом простом примере стандартной конфигурации вполне достаточно. Визуализация веб-страниц Выводом предыдущего примера была не НТМL-разметка. а просто строка World". "Hello Чтобы сгенерировать НТМL-ответ на запрос браузера, понадобится создать представление, которое сообщает MVC, каким образом генерировать ответ на запрос, поступивший из браузера. Создание и визуализация представления Прежде всего. необходимо модифицировать метод действия но в листинге 2.3. Index (),как показа­ Для простоты восприятия в этом и во всех будущих листингах из­ менения выделяются полужирным. Листинг 2.3. Визуализация представления в файле из папки Controllers using Microsoft.AspNetCore.Mvc; namespace Partyinvites.Controllers Homecontroller. cs 40 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 puЫi c class HomeContro ller : Co ntro ller { ViewResul t Index () return View ( "МyView") ; puЬlic Возвращая из метода действия объект визуалuзацuu представления. Объект ViewResul t . мы инструктируем MVC о ViewResult создается посредством вызова метода View () с указанием имени представления . которое должно применяться, т.е. MyView. Запустив приложение, можно заметить. что инфраструктура MVC пытается найти представление, как отражено в сообщении об ошибке на рис. 2.9. An unhandled exception occurred while processing the request. lnvalidOpe1·at,onExceptюn: Т11е ve\v 'MyView· was not found. TN: follo\чing locations \-vere searched: /Vie~vs/1-iome/MyV'ew.cshtml /Views/Sha1·ed/MyView.cshtm' Microso~Asp etCore.MV<:.Vie•'IEng1nes.Vi€\vEngiмResult.EnsureSuccess'ul(IEnum~rable<stnng> Q11ery Рис. Cnokies 2.9. original ocations) HPadPr< Инфраструктура MVC пытается найти представление Сообщение об ошибке очень полезно. Оно объясняет. что инфраструктура MVC не смогла найти представление. указанное для метода действия. и показывает. где произ­ водился поиск . Представления хранятся в подпапках внутри папки представления. которые связаны с контроллером Views / Home. Vi ews . Например. содержатся в папке по имени Представления, не являющиеся специфическими для отдельного конт­ роллера, хранятся в папке под названием ет папки Home , Home и Sha red (Model-View-Controller). Views/Shared. Среда Visual Studio созда­ Web Application автоматически, когда используется шаблон и в рамках начальной подготовки проекта помещает в них не­ сколько представлений-заполнителей. Чтобы создать представление. необходимое для текущего примера. раскроем пап­ ку Vi e ws в окне Solutioп Explorer. щелкнем правой кнопкой мыши на папке и выберем в контекстном меню пункт Add'~ Среда New ltem Ho me (Добавитьq Новый элемент). Visual Studio предложит список шаблонов элементов. Добравшись до катего­ ASP.NET CoreqWebqдSP.NET в панели слева, выберем элемент MVC View Page (Страница представления МVС) в центральной панели (рис. 2. 1О). (Не следует приме­ нять шаблон Razor Page (Страница Razor), который не относится к МVС Framework.) рии Глава 2. Ваше первое приложение MVC Add №w Jtem - Portylnv•t« ' Sortby:~ " lnstalled ~ ASP .NП ~· !.; Cort Code w.ь ASP.NEТ Ш)' General S<ripts Content ~ Controller Сlш ~· W•b API Controller Сlш !.; Gener4I " МVС -- [@ ··1"' Online ASP.NET Coro Core Туре: ASP.NfТ MVC View P•g• ASP.NET Core ASP.NET Core R.zor P•g• ~ М'/С Vi•w Р•9• ASP.NEТ Core Ш)' MVC View Layout P•g• ASP .NEТ ~· • MVC V1ew Storl P•ge ASP .NEТCoro lfl" ' MVC V.ow lmports Poge ASP.NEТ ~- Razor Tag Helper ASP .NEТCoro ~ ><: Р· Se•rch (Ctrl•f) ... 41 Core Coro MyV.ew.<Shtml №me:. Рис. Совет. В папке Views 2. 1О. Создание представления уже есть несколько файлов, которые были добавлены целью предоставления начального содержимого, показанного на рис. Visual Studio 2.6. с Такие файлы можно спокойно проигнорировать. Введем в поле Name (Имя) имя My Vi ew . cshtml и щелкнем на кнопке Add (Добавить) Visual Studio создаст файл V i ew s / Ho me / для создания представления. Среда MyVi e w. cs h t ml и откро ет его для р едактиров ания. Н ачальное содержимо е ф айла - это просто ряд ком м ентариев и заполнитель. Заменим его с одер ­ представления жимым, приведенным в листинге 2.4. Совет. Довольно легко создать файл представления не в той папке. Если в итоге вы не по­ лучили файл по имени MyV iew. c sh t ml в папке View s / Home, тогда удалите созданный файл и попробуйте создать заново. Листинг 2.4. Замена содержимого файла МyView. cshtml из папки Views/Home @{ La y ou t null; < !DOCTYPE h t ml > <h t ml > <head > <me ta name="vi ewpor t" content="width=dev i ce - wi d th" / > <title>Index< / title> </ head> <b ody> <d i v > 42 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Hello World (from the view) </div> </body> </html> Теперь файл представления содержит главным образом НТМL-разметку. Исключением является часть, которая имеет следующий вид: @{ Layout null; Такое выражение будет интерпретироваться механизмом визуализации Razor, ко­ торый обрабатывает содержимое представлений и генерирует НТМL-разметку. от­ правляемую браузеру. Показанное выше простое выражение му Razor Razor сообщает механиз­ о том, что компоновка не применяется; компоновка похожа на шаблон для НТМL-разметки, который посылается браузеру (и будет описан в главе проигнорируем механизм Razor 5). Мы пока и возвратимся к нему позже. Чтобы увидеть создан­ ное представление, выберем в меню Debug пункт Start Debugging 2.11. для запуска прило­ жения. Должен получиться результат, приведенный на рис. С @)!Oёathost:57628 _ _ _ _ _ _ _ _ _ _. Hello World (from t11e view) Рис. 2.11. Тестирование представления При первом редактировании метод действия Index () возвращал строковое значе­ ние, так что инфраструктура МVС всего лишь передавала браузеру строковое значение в том виде. как есть. Теперь, когда метод инфраструктура MVC Index () возвращает объект ViewResult, визуализирует представление и возвращает сгенерированную НТМL-разметку. Мы сообщили инфраструктуре MVC о том, какое представление должно использоваться, поэтому с помощью соглашения об именовании она автома­ тически выполнила его поиск. Соглашение предполагает. что имя файла представле­ ния совпадает с именем метода действия, а файл представления хранится в папке, названной по имени контроллера: /Views/Home/MyView.cshtml. ViewResul t методы действий могут возвращать другие ре­ зультаты. Например, если мы возвращаем объект RedirectResul t, то браузер будет перенаправлен на другой URL. Если мы возвращаем объект HttpUnauthorizedResul t, Кроме строк и объектов то предлагаем пользователю войти в систему. Все вместе такие объекты называются результатами действий. Система результатов действий позволяет инкапсулировать и повторно использовать часто встречающиеся ответы в действиях. В главе 17 рассмотрим их подробнее и продемонстрируем разные способы их применения. мы Глава 2. Ваше первое приложение MVC 43 Добавление динамического вывода Весь смысл платформы для разработки веб-приложений состоит в конструирова­ нии и отображении динамического вывода. В рамках МVС работа контроллера заклю­ чается в подготовке данных и передаче их представлению, которое отвечает за их визуализацию в виде НТМL-разметки. Один из способов передачи данных из контроллера в представление предусмат­ ViewBag, который является членом базового ViewBag - это динамический объект. в котором ривает использование объекта Controller. По существу класса можно устанавливать произвольные свойства. делая их значения доступными в любом визу­ ализируемом далее представлении. В листинге 2.5 способом простых динамических данных в файле Листинг 2.5. Установка данных представления в из папки демонстрируется передача таким HomeController. cs. HomeController.cs Controllers using System; using Microsoft.AspNetCore.Mvc; narnespace Partylnvites.Controllers puЫic class HorneController : Controller ViewResult Index() ( int hour = DateTime.Now.Hour; ViewBag. Greeting = hour < 12 ? "Good return View("MyView"); puЬlic Мorning" "Good Afternoon"; Данные для представления указываются во время присваивания значения свойс· тву ViewBag. Greeting. Свойство Greeting не существует вплоть до момента. ког­ да ему присваивается значение. что позволяет передавать данные из контроллера в представление в свободной и гибкой манере. без необходимости в предварительном определении классов. Чтобы получить значение данных. необходимо еще раз сослать­ ся на свойство ге 2.6, ViewBag. Greeting, но уже в представлении. как показано в листин­ содержащем изменение. которое бьmо внесено в файл Листинг 2.6. MyView. cshtml. Извлечение значения данных ViewВag в файле МyView. из папки Views/Home @( Layout = null; <!DOCTYPE htrnl> <htrnl> <head> <rneta narne="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> cshtml 44 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 @ViewBag.Greeting World (from the view) </div> </body> </html> Добавленный в листинге фрагмент - это выражение Razor, которое оценивается. View () в Index () контроллера приводит к тому. что MVC находит файл представления MyV iew. csh tml и запрашивает у механизма визуализации Razor синтаксический анализ содержимого данного файла. Механизм Razor ищет выражения, подобные до­ бавленному в листинге 2.6, и обрабатывает их. В рассматриваемом примере обработ­ когда MVC применяет представление для генерации ответа. Вызов метода методе ка выражения означает вставку в представление значения. которое было присвоено свойству ViewBag. Greeting внутри метода действия . Выбор для свойства имени Greeti ng не диктуется какими-то особыми соображе­ ниями. Его можно было бы заменить любым другим именем. и все работало бы точно так же при условии, что имя, используемое в контроллере. совпадает с именем. ко­ торое применяется в представлении. Присваивая значения более чем одному свойс­ тву, можно передавать из контроллера в представление множество значений данных. После запуска проекта будет виден результат внесенных изменений (рис. 2.12). С /~alhost:57628 Good Afteгnoon Woгld Рис. 2.12. (fгoin tl1e vie'-"') Динамический ответ, сгенерированный MVC Создание простого приложения для ввода данных В оставшихся разделах главы будут исследованы другие базовые функциональные средства MVC за счет построения простого приложения для ввода данных. В этом раз ­ деле мы собираемся несколько увеличить темп изложения. Целью является демонс­ трация инфраструктуры МVС в действии, а потому некоторые объяснения того. что происходит "за кулисами", будут пропущены. Однако не беспокойтесь - мы вернемся к подробному обсуждению данных тем в последующих главах. Предварительная настройка Представьте себе, что ваша подруга решила организовать вечеринку в канун ново ­ го года и попросила создать веб - приложение, которое позволяет приглашенным отве ­ тить на приглашение по электронной почте. Она высказала пожелание относительно четырех основных средств, которые перечислены ниже: • • домашняя страница, отображающая информацию о вечеринке; форма, которая может использоваться для ответа на приглашение (гepondez vous plait - RSVP): s'il Глава 2. Ваше первое прило жение MVC • проверка достоверности для формы RSVP. 45 которая будет отображать страницу с выражением благодарности за внимание; • итоговая страница, которая показывает, кто собирается прийти на вечеринку. В последующих разделах мы достроим проект MVC , созданный в начале главы, и добавим в него перечисленные выше средства. Первый пункт можно убрать из спис­ ка. применив то, что было показано ранее - добавить НТМL-разметку с подробной информацией о вечеринке в существующее представление . В листинге содержимое файла Листинг Vi ews / Home / MyView . cshtml 2.7 приведено с внесенными дополнениями. 2. 7. Отображение подробностей о вечеринке в файле МyView. cshtml из папки Views/Home @( La yout = nu l l; <IDOCTYPE html > <html> <head> <meta name=" v iewport" c ontent="width=de vice - width" / > <title>Inde x</title > </head > <b ody> <div> @ViewBag.Greeting Worl d ( f rom the vie w) <p>We' re going to have an exci ting party. <br /> (То do: sell i t better. Add pictures or something.) </р> </ div > </ b ody> </ html > Мы двигаемся в верном направлении. Если запустить приложение, выбрав в меню Debug пункт Start Debugging, то отобразятся подробности о вечеринке полнитель для подробностей. но сама идея должна быть понятной (рис. Good After11oon World (froш точнее, за­ - 2.13). the view) We'Ie goi11g to have an exciting party. (То do: sell it bettel". Add pich1Ies or sornetl1iпg. ) Рис. 2.1 З. Добавление информации о вечеринке в НТМL- разметку представления Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 46 Проектирование модели данных Буква "М" в аббревиатуре МVС обозначает тodel (модель), которая является самой важной частью приложения. Модель - это представление реальных объектов, про­ цессов и правил, которые определяют сферу приложения, известную как предметная область. Модель, которую часто называют моделью предметной области, содержит объекты С# (или объекты предметной области), образующие "вселенную" приложе­ ния, и методы. позволяющие манипулировать ими. Представления и контроллеры открывают доступ клиентам к предметной области в согласованной манере, и любое корректно разработанное приложение MVC начинается с хорошо спроектированной модели, которая затем служит центральным узлом при добавлении контроллеров и представлений. Для проекта Partyinvi tes сложная модель не требуется, поскольку приложение совсем простое. и нужно создать только один класс предметной области, который по­ лучит имя GuestResponse. Такой объект будет отвечать за хранение. проверку досто­ верности и подтверждение ответа на приглашение По соглашению имени Web (RSVP). классы, которые образуют модель, помещаются в папку по которую среда Models, шаблона MVC Visual Studio создает автоматически в случае выбора Applicatioп (Model-View-Coпtroller). Чтобы создать файл класса, щелкнем правой кнопкой мыши на папке Solutioп Explorer и выберем в контекстном меню пункт Назначим новому классу имя GuestResponse. cs Add9Class Models в окне (Добавить9Класс). и щелкнем на кнопке Add (Добавить). 2.8. Приведем содержимое нового файла класса к виду, показанному в листинге Листинг 2.8. Содержимое файла GuestResponse.cs из папки Models namespace Partyinvites.Models puЫic class GuestResponse { puЬlic puЫic puЫic puЫic string Name { get; set; string Email { get; set; ) string Phone { get; set; ) bool? WillAttend { get; set; Совет. Вы могли заметить, что свойство т.е. оно может принимать значение WillAttend имеет тип bool, допускающий null, true, false или null. Обоснование такого решения будет приведено в разделе "Добавление проверки достоверности" далее в главе. Создание второго действия и строго типизированного представления Одной из целей разрабатываемого приложения является включение формы RSVP, что означает необходимость определения метода действия, который сможет получать запросы к форме. В единственном классе контроллера можно определять множество методов действий, а по соглашению связанные действия группируются вместе в од­ ном контроллере. В листинге к контроллеру Ноте. 2.9 иллюстрируется добавление нового метода действия Глава 2. Ваше первое приложение MVC Листинг 2.9. Добавление метода действия в файле из папки 47 HomeController. cshtml Controllers using System; using Microsoft.AspNetCore.Mvc; namespace Partyinvites.Controllers puЫic class HomeController : Controller ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" return View ( "MyView") ; puЫic "Good Afternoon"; ViewResul t RsvpForm () return View () ; puЬlic Метод действия RsvpForm () вызывает метод View () без аргументов, что сообща­ ет инфраструктуре МVС о необходимости визуализации стандартного представления. связанного с этим методом действия, которым будет представление с таким же име­ нем, как у метода действия (RsvpForm. cshtml в данном случае). Views/Home и выберем Щелкнем правой кнопкой мыши на папке в контекстном MVC View MVC). укажем RsvpForm. cshtml в качестве имени нового файла и щелкнем на кнопке Add (Добавить), чтобы создать файл. Приведем содержимое нового файла в соответствие с листингом 2.10. меню пункт Add~ New Page ltem (Добавить~ Новый элемент). Выберем шаблон (Страница представления Листинг Содержимое файла 2.10. RsvpForm. cshtml из папки Views/Home @model Partyinvites.Models.GuestResponse @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RsvpForm</title> </head> <body> <div> This is the RsvpForm.cshtml View </div> </body> </html> Содержимое состоит в основном из НТМL-разметки. но с добавлением Rаzоr­ выражения @model, которое используется для создания строго типизированного представления. Строго типизированное представление предназначено для визуали- 48 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 зации специфического типа модели , и если указан желаемый тип (в данном случае класс GuestResp o n se из пространства имен Pa r ty l nv i tes . Models), то MVC может со здать ряд удобных сокраще ний , чтобы сделать его проще. Вскоре мы задействуем преимущество характеристики строгой типизации . Чтобы протестировать новый метод действия и его представление , запустим при­ ложение, выбрав в меню Debug пункт Start Debugging, и с помощью бр аузера перей­ URL вида / Horne/RsvpForrn. Инфраструктура MVC применит описанное ранее соглашение об именовании для дем на направления запроса методу действия Rs vpFo r m () , Н оте . Данный метод дей ствия указывает MVC определенному в контроллере о том . что должно визуализировать­ ся стандартное представление , которое посредством еще одного применения того же соглашения об именов ании ви зуализирует Результат показ ан на рис . С 1 <D Rsvp Fo r rn . cs hml из папки Views/Home . 2. 14. localhos ·Sl628/Home/RsvpForm This is the RsvpFoп11.csht111l View Рис. 2.14. Визуализация второго представления Ссылка на методы действий Нам необходимо со здать в представлении деть представление Rsvp Forrn специфиче с кий метод действия (листинг Листинг 2.11. ссылку. чтобы гости могли ви­ URL, который указывает на 2.11) . Добавление ссылки на форму из папки MyView без обязательного знания RSVP в файле МyView. cshtml Views/Home @( Layout = nul l ; < ! DOCTY PE html> <h tml > <h e ad> <me ta name= "v i ewport " cont e nt= "wi dth=device- width " /> <ti t l e> Index</tit le > </ head> <b ody> <d i v> @ViewBag . Gre eting World (from th e v i ew) <p >We ' re going to have a n exci t ing party . <br /> (То do: se ll it be t t er. Add pic tures or some thin g.) </р> Глава 2. Ваше первое приложение MVC 49 <а asp-action="RsvpForm">RSVP Now</a> </div> </body> </html> В листинге добавлен элемент а, который имеет атрибут 2.11 asp-action . Данный атрибут является примером атрибута вспомогательной функции дескриптора, т.е. инструкцией Razoг, которая будет выполнена, когда представление визуализируется. Атрибут это инструкция по добавлению к элементу а атрибута asp-action - содержащего URL href, для метода действия. Работа вспомогательных функций дескрип­ торов объясняется в главах 24-26, а пока достаточно знать, что asp-action - про­ стейший вид атрибута вспомогательной функции дескриптора для элементов а. Он указывает Razor на необходимость вставки URL для метода действия, определенного в том же контроллере, для которого визуализируется текущее представление. Запустив проект, можно увидеть ссылку. которую создала вспомогательная функция дескрип­ тора (рис. 2.15). С Good Mon1ing \\iorld (from the \·ie\\·) \Ve're going to ha\"e а.11 exciting party. (То do: sell it bener. Add pictures or something.) Рис. 2.15. !' ~ ~ocalh?s_ 576~8~ome/~svp:crn~ "tr This is the Rs\·pForm.cshtml \ 'ie\\" Ссылка на метод действия Если после запуска приложения навести курсор мыши на ссылку RSVP Now (Ответить на приглашение) в окне браузера, то можно заметить, что ссылка указы­ вает на следующий URL (возможно, вашему проекту Visual Studio назначит другой номер порта): http://localhost:57628/Home/RsvpForm Здесь требуется соблюдать один важный принцип: вы должны использовать средс­ тва. предлагаемые MVC для генерации URL, а не жестко кодировать их в своих пред­ ставлениях. Когда вспомогательная функция дескриптора создает атрибут hre f для элемента а. она инспектирует конфигурацию приложения. чтобы выяснить, каким должен быть URL. В итоге появляется возможность изменять конфигурацию прило­ жения с целью поддержки разных форматов URL без необходимости в обновлении ка­ ких-либо представлений. Особенности работы такого механизма рассматриваются в главе 15. Построение формы Теперь, когда строго типизированное представление создано и достижимо из представ­ ления Index, займемся подгонкой содержимого файла RsvpForm. cshtml , чтобы превра­ GuestResponse (листинг 2.12). тить его в НТМL-форму для редактирования объектов 50 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC 2 Листинг 2.12. Создание представления в виде формы в файле из папки RsvpForm. csh tml Views/Home @model Partylnvites.Models.GuestResponse @{ Layout = null; <IDOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RsvpForm</title> </head> <body> <form asp-action="RsvpForm" method="post"> <р> <laЬel asp-for="Name">Your <input asp-for="Name" /> name:</laЬel> </р> <р> <laЬel asp-for="Email">Your <input asp-for="Email" /> email:</laЬel> </р> <р> <laЬel asp-for="Phone">Your <input asp-for="Phone" /> phone:</laЬel> </р> <р> <laЬel>Will you attend?</laЬel> <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> </р> <Ьutton type="suЬmit">SuЬmit RSVP</button> </form> </body> </html> Для каждого свойства класса модели и input (или элемент select GuestResponse определены элементы label WillAttend). Каждый элемент ассо­ в случае свойства циирован со свойством модели с применением еще одного атрибута вспомогательной функции дескриптора - asp-for. Атрибуты вспомогательных функций дескрипторов конфигурируют элементы, чтобы привязать их к объекту модели. Вот пример НТМL­ разметки, которую генерируют вспомогательные функции дескрипторов для отправки браузеру: <р> <label for="Name">Your name:</label> <input type="text" id="Name" name="Name" value=""> </р> 51 Глава 2. Ваше первое приложение MVC Атрибут asp-for asp-for в элементе в элементе label устанавливает значение атрибута устанавливает атрибуты i nput id и name. for . Атрибут В данный момент это не выглядит особенно полезным, но по мере определения прикладной функциональ­ ности вы увидите, что ассоциирование элементов со свойством модели предлагает дополнительные преимущества. Более непосредственный результат дает атрибут который использует конфигурацию маршрутизации атрибута ac tion в URL, asp -action URL в элементе form, приложения для установки нацеленный на специфический метод действия . например: <form method="p o st" actio n ~" / Home/RsvpForm"> Как и в случае атрибута вспомогательной функции дескриптора, примененного к элементу а, преимущество такого подхода заключается в том, что вы можете изме­ нять систему URL, используемую приложением, и содержимое, которое генерируется вспомогательными функциями дескрипторов, автоматически отразит изменения. Форму можно увидеть, запустив приложение и щелкнув на ссылке (рис. RSVP Now 2. 16). f- <D localhost:S7628/Home/RsvpForn1 С ----=-==iJ Your name: [ Your email: ___ Your phone: [ \\7i.ll _ -:=J you attend? Гchoose В_!) option т { Submit RSVP Рис. j 2.16. Добавление НТМL-формы к приложению Получение данных формы Мы пока еще не указали инфраструктуре MVC. что должно быть сделано, когда форма отправляется серверу. При нынешнем состоянии приложения щелчок на кноп­ ке Submit RSVP (Отправить RSVP) лишь очищает любые значения. введенные внутри формы . Причина в том, что форма осуществляет обратную отправку методу действия Rs vpForm () контроллера Home, который только сообщает MVC о необходимости пов­ торной визуализации представления. Чтобы получить и обработать отправленные данные формы, мы собираемся вос­ пользоваться основной возможностью контроллера. Мы добавим второй метод дейс­ твия RsvpForm ( ) . чтобы получить в свое распоряжение следующие возможности. 52 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 • Метод. который отвечает н.а НТ'ГР-запросы GET. Каждый раз, когда кто-то щел­ GET. Эта версия действия будет кает на ссылке, браузер обычно выдает запрос отвечать за отображение изначально пустой формы, когда кто-нибудь впервые посещает /Home/RsvpForm. • Метод, который отвечает н.а НТ'ГР·запросы POST. По умолчанию формы. ви­ зуализированные с помощью Html. BeginForm (), отправляются браузером как запросы POST. Эта версия действия будет отвечать за получение отправленных данных и принятие решения о том, что с ними делать. Обработка запросов GET и POST в отдельных методах С# способствует обеспече­ нию аккуратности кода контроллера, т.к. описанные выше два метода имеют разные обязанности. Оба метода действий вызываются через тот же самый URL, но в зави­ - GET или POST - инфраструктура MVC вызывает подходя­ 2.13 показаны изменения, которые необходимо внести в класс симости от вида запроса щий метод. В листинге HomeController. Листинг 2.1 З. Добавление метода в файле HomeController. cs из папки Controllers using System; using Microsoft.AspNetCore.Mvc; using Partyinvites.Models; namespace Partyinvites.Controllers puЫic class HomeController : Controller ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag. Greeting = hour < 12 ? "Good Morning" return View("MyView"); puЬlic "Good Afternoon"; [HttpGet] ViewResult RsvpForm() { return View (); puЫic [HttpPost] ViewResult RsvpForm(GuestResponse guestResponse) puЫic 11 Что сделать: сохранить ответ от гостя return View(); Существующий метод действия который указывает запросов GET. RsvpForm () был снабжен атрибутом HttpGet, на то, что данный метод должен применяться толыю для Затем была добавлена перегруженная версия метода нимающая объект сообщает MVC MVC GuestResponse. RsvpForm (),при­ HttpPost, который только с запросами POST. К ней был применен атрибут о том, что новый метод будет иметь дело Произведенные добавления объясняются в последующих разделах. Кроме того, было импортировано пространство имен чтобы на тип модели Partyinvi tes. Models. Это сделано для того. GuestResponse можно было ссылаться без необходимости в указании полностью определенного имени класса. Глава 2. Ваше nервое nриложение MVC 53 Использование привязки модели Первая перегруженная версия метода действия самое представление, что и ранее (файл показанной на рис. 2.16. RsvpForm () визуализирует то же RsvpForm. cshtml), для генерации формы, Вторая перегруженная версия более интересна из-за нали­ чия параметра. Но с учетом того, что данный метод действия будет вызываться в от­ вет на НТГР-запрос POST, а тип GuestResponse является классом С#, каким образом они соединяются между собой? Секрет кроется в привязке модели -удобной функциональной возможности МVС, пос­ редством которой производится разбор входящих данных и применение пар "ключ/зна­ чение" в НТГР-запросе для заполнения свойств в типах моделей предметной области. Привязка модели - мощное и настраиваемое средство, которое избавляет от кропот­ ливого и тяжелого труда по взаимодействию с НТГР-запросами напрямую и позволяет работать с объектами С#, а не иметь дело с индивидуальными значениями данных, от­ правляемыми браузером. Объект RsvpForm ( ) GuestResponse, который передается методу действия в качестве параметра, автоматически заполняется данными из полей фор­ мы. Привязка модели, включая ее настройку. подробно рассматривается в главе 26. Одной из целей приложения является предоставление итоговой страницы с дета­ лями о том, кто придет на вечеринку, что означает необходимость сохранения полу­ чаемых ответов. Мы собираемся делать это за счет создания в памяти коллекции объ­ ектов. В реальном приложении такой подход не подойдет, т.к. данные ответов будут утрачиваться в результате останова или перезапуска приложения, но он позволяет сосредоточить внимание на MVC и создать приложение, которое может быть легко сброшено в свое начальное состояние. Совет. В главе 8 будет продемонстрировано использование MVC для постоянного хранения и доступа к данным как часть более реалистичного примера приложения под названием SportsStore. Мы добавили в проект новый файл, щелкнув правой кнопкой мыши на папке Models ет имя и выбрав в контекстном меню пункт Add~Class (Добавить~Класс). Файл име­ Reposi tory. cs Листинг 2. 14. и содержимое, показанное в листинге Содержимое файла Reposi tory. cs из папки 2.14. Models using System.Collections.Generic; namespace Partyinvites.Models ( puЫic static class Repository private static List<GuestResponse> responses = new List<GuestResponse>(); puЫic static IEnumeraЫe<GuestResponse> Responses get ( return responses; puЬlic static void AddResponse(GuestResponse response) ( responses.Add(response); 54 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Класс Reposi tory и его члены объявлены статическими, чтобы облегчить со­ хранение и извлечение данных в разных местах приложения. Инфраструктура MVC предлагает более сложный подход к определению общей функциональности, называ­ емый внедрением зависимостей, который будет описан в главе 18, но для простого приложения вроде рассматриваемого вполне достаточно и статического класса. Сохранение ответов Теперь, когда есть куда сохранять данные, можно обновить метод действия, кото­ рый получает НТТР-запросы Листинг POST (листинг 2.15). Обновление метода действия в файле 2.15. из папки HomeController. cs Controllers using System; using Microsoft.AspNetCore.Mvc; using Partylnvites.Models; namespace Partylnvites.Controllers puЫic class HomeController : Controller ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" return View ( "MyView") ; puЬlic "Good Afternoon"; [HttpGet] ViewResult RsvpForm() { return View () ; puЬlic [HttpPost] ViewResult RsvpForm(GuestResponse guestResponse) { Repository.AddResponse(questResponse); return View("Thanks", questResponse); puЫic Все, се - ект что необходимо сделать с данными формы, передать методу GuestResponse, Reposi tory. AddResponse () отправленными в запро­ в качестве аргумента объ­ который был передан методу действия, чтобы ответ мог быть сохранен. Почему привязка модели не похожа на инфраструктуру Web Forms? В главе 1 упоминалось, что одним из недостатков традиционной инфраструктуры ASP.NET Web Forms было сокрытие деталей НТТР и HTML от разработчиков. Вас может интересовать, делает ли то же самое привязка модели MVC, которая применялась для создания объекта GuestResponse из НТТР-запроса POST в листинге 2.15. Нет, не делает. Привязка модели освобождает нас от решения утомительной и подвержен­ ной ошибкам задачи по инспектированию НТТР-запроса и извлечению всех требующихся значений данных, но (что самое важное) при желании мы могли бы обрабатывать запрос Глава 2. Ваше nервое nриложение MVC вручную, поскольку 55 MVC обеспечивает легкий доступ ко всем данным запроса. Ничто не скрыто от разработчика, но есть несколько удобных средств, которые упрощают работу с НТТР и HTML; тем не менее, использовать их вовсе не обязательно. Это может показаться едва заметной разницей, но по мере углубления знаний инфраструк­ туры MVC вы увидите, что практика разработки в ней полностью отличается от традици­ Web Forms. Вы всегда будете осведомлены относительно того, как онной инфраструктуры обрабатываются получаемые приложением запросы. Вызов метода View () внутри метода действия RsvpForm () сообщает MVC о том, Thanks и передать ему объ­ что нужно визуализировать представление по имени ект GuestResponse. Для создания упомянутого представления щелкнем правой кнопкой мыши на папке Views/Home в окне Solution Explorer и выберем в контекс­ тном меню пункт Add~New ltem (Добавить~Новый элемент). Укажем шаблон MVC View Page (Страница представления MVC) из категории ASP.NET, назначим ему имя Thanks. csh tml и щелкнем на кнопке Add (Добавить). Среда Visual Studio создаст файл Views/Home/Thanks. cshtml и откроет его для редактирования. Поместим в файл содержимое, приведенное в листинге Листинг 2.16. Содержимое файла Thanks. cshtml из папки Views/Home 2.16. @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!</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. </р> <p>Click </body> </html> asp-action="ListResponses ">here</ а> to see who is coming. </р> <а Представление Thanks. cshtml применяет механизм визуализации Razor для GuestResponse, которое пе­ RsvpForm (). Выражение @model синтаксиса Razor отображения содержимого на основе значения свойства редается методу View () внутри указьmает тип модели предметной области, с помощью которого представление стро­ го типизировано. Для доступа к значению свойства в объекте предметной области используется конструкция Model. ИмяСвойства. Например, чтобы получить значение свойства Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 56 Name, применяется Model. Name. Не беспокойтесь , если синтаксис Razor пока не по­ нятен - он более подробно объясняется в главе 5. Thanks. мы получили базовый пример об­ MVC. Запустим приложение в Visual Studio, выбрав в Теперь, когда создано представление работки формы посредством меню Debug пункт Start Debugging, занный на рис. 2.17 RSVP Now, введем на форме Submit RSVP. Появится результат, пока­ щелкнем на ссылке какие-нибудь данные и щелкнем на кнопке (он может отличаться, если введено другое имя либо указано о невозможности посетить вечеринку) . Thank you, Joe! Your email: ~oe@examp.!!:co'!:._ lt's great that you're coming. The drinks are a!ready in the fi-idge! ..:___:: ... Yourphone:@SS-1234 \\"ill you attend? YeS.Thьethere Click ~ to see \\'ho is coming. •1 [sub~itRSVP] Рис. 2. 17. Представление Than ks Отображение ответов В конце представления Thanks . c shtml мы добавили элемент а для создания ссылки, которая позволяет отобразить список людей , собирающихся посетить вече ­ ринку . С применением атрибута вспомогательной функции дескриптора создается URL, <p>Click который нацелен на метод действия по имени <а asp - ac t i o n Li st Respons es (): asp-action="ListResponses">here< /a > to see who is coming. </р > Наведя курсор мыши на ссылку, которую отображает браузер. легко заметить, что URL вида /Home/ ListResponses . Это не соответствует ни одному Home, и если щелкнуть на ссьтке. то появится стра­ ница ошибки "404 - Not Found" (не найдено) . Мы устраним проблему путем создания в контроллере Home метода действия. на который нацелен URL (листинг 2. 17). она указывает на м етоду действия в контролл е ре Листинг 2.17. Добавление метода действия в файле HomeController . cs using using using using Sys tem ; Microsoft.AspNetCore.Mvc ; Partylnvites.Models; Syst0111.Linq; namespace Partyln vites .Con t rollers puЬlic c lass Ho me Co n tro l le r : Contro ll er Глава 2. Ваше nервое nриложение MVC ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" return View ( "MyView") ; 57 puЫic "Good Afternoon"; [HttpGet] ViewResult RsvpForm() { return View () ; puЫic [HttpPost] ViewResult RsvpForrn(GuestResponse guestResponse) { Repository.AddResponse(guestResponse); return View("Thanks", guestResponse); puЫic ViewResult ListResponses() { return View(Repository.Responses.Where(r => r.WillAttend puЬlic == true)); ListResponses ();он вызывает метод View (), Reposi tory. Responses в качестве аргумента. Именно так метод Новый метод действия называется используя свойство действия предоставляет данные строго типизированному представлению. Коллекция объектов GuestResponse фильтруется с применением LINQ, так что используются только ответы с положительным решением об участии в вечеринке. ListResponses () Метод действия не указывает имя представления, которое должно применяться для отображения коллекции объектов GuestResponse, поэто­ му будет задействовано соглашение об именовании и МVС инициирует поиск пред­ ставления по имени ListResponses. cshtrnl в папках Views/Horne и Views/Shared. Views/ Чтобы создать представление, щелкнем правой кнопкой мыши на папке Horne в окне Solution Explorer и выберем в контекстном меню пункт Add~New (Добавить~Новый элемент). Выберем шаблон ния MVC) из категории нем на кнопке Add ASP.NET, 2.18. ListResponses. cshtrnl и щелк­ 2.18. Отображение принятых приглашений в файле из папки @model назначим ему имя ltem (Страница представле­ (Добавить). Приведем содержимое нового файла представления в соответствие с листингом Листинг MVC View Page ListResponses. cshtml Views/Home IEnumeraЬle<Partyinvites.Models.GuestResponse> @{ Layout = null; <!DOCTYPE html> <htrnl> <head> <rneta narne="viewport" content="width=device-width" /> <title>Responses</title> </head> 58 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 <body> <h2>Here is t he list of people attending t he pa rty </ h2> <tаЫе> <thead> <tr> <th>Name</th> <th>Email</th> <t h>Phone</th > </tr> </thead> <tbody> @foreach ( Partyinvites.Mo dels.GuestResp on se r in Mode l) { <tr> <td>@r.Name</td> <td >@ r.Email </ td > <td>@r . Phone</td > </tr> </tbody> </tаЫе> </body> </html> Файлы представлений Razor имеют расширение . csh tml. потому что содержат HTML. Это можно заметить в листинге 2.18, где исполь­ смесь кода С# и элементов зуется цикл foreach для обработки всех объектов Gu estResponse , которые метод View (). В отличие от нор­ forea ch из Razor содержит элементы действия передает представлению с применением метода мального цикла HTML. языка С# тело цикла foreach добавляемые к ответу. который будет отправлен обратно браузеру . В данном представлении для каждого объекта рый содержит элементы td, GuestRespo nse генерируется элемент Чтобы увидеть список в работе, запустим приложение, выбрав в меню Start Debugging, tr. кото­ Debug пункт заполненные значениями свойств объекта . отправим какие-то данные формы и затем щелкнем на ссьшке для про­ смотра списка ответов. Отобразится сводка по данным , введенным вами с момента за­ пуска приложения (рис. 2.18). Представление не оформляет данные привлекательным образом, но пока этого вполне достаточно, а стилизацией мы займемся позже в главе. Responses Here is the list of people attending the party ~ame Email Phone Joe [email protected] 555-1234 Alice [email protected] 555-5678 Рис. 2.18. Отображение сnиска участников вечеринки Глава 2. Ваше первое приложение MVC 59 Добавление проверки достоверности Теперь мы готовы добавить в приложение проверку достоверности вводимых дан­ ных. В отсутствие проверки достоверности пользователи смогут вводить бессмыслен­ ные данные или даже отправлять пустую форму. В приложении MVC проверка до­ стоверности обычно применяется к модели предметной области, а не производится в пользовательском интерфейсе. Это значит. что проверка достоверности определя­ ется в одном месте, но оказывает воздействие в приложении везде, где используется класс модели. Инфраструктура MVC поддерживает декларативные правил.а проверки достоверности, определяемые с помощью атрибутов из пространства имен CornponentModel. DataAnnotations, ражаются посредством стандартных атрибутов С#. В листинге применить такие атрибуты к классу модели Листинг из папки using 2.19 показано, как GuestResponse. Применение проверки достоверности в файле 2.19. Systern. т.е. ограничения проверки достоверности вы­ GuestResponse. cs Models System.Componentмodel.DataAnnotations; namespace Partylnvites.Models { 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 string Email { get; set; } [Required (ErrorMessage = "Please enter your phone numЬer")] puЫic //Пожалуйста, введите свой номер телефона string Phone { get; set; } [Required (ErrorМessage = "Please specify whether you' 11 attend")] puЫic //Пожалуйста, puЫic укажите, примете ли участие bool? WillAttend { get; set; } Инфраструктура МVС автоматически обнаруживает атрибуты проверки достовер­ ности и использует их для проверки данных во время процесса привязки модели. Мы импортировали пространство имен, которое содержит атрибуты проверки достовер­ ности, так что к ним можно обращаться, не указывая полные имена. Совет. Как отмечалось ранее, для свойства WillAttend был выбран булевский тип, до­ пускающий null. Мы поступили так для того, чтобы появилась возможность применять атрибут проверки достоверности Required. Если бы использовался обычный булевский тип, то значением, получаемым посредством привязки модели, могло быть только true или false, и отсутствовала бы возможность определить, выбрал ли пользователь значе­ null, имеет три разрешенных значения: true, false null. Браузер отправляет null, если пользователь не выбрал значение, и тогда атри­ бут Required сообщит об ошибке проверки достоверности. Это хороший пример того, насколько элегантно инфраструктура MVC сочетает средства С# с HTML и НТТР. ние. Булевский тип, допускающий и 60 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Проверку на наличие проблемы с достоверностью данных можно выполнить с применением свойства ModelState. IsValid в классе контроллера. В листинге показано, как она реализована в методе действия просы POST, Листинг внутри класса контроллера 2.20 ().поддерживающем за­ Home. Проверка на наличие ошибок проверки достоверности в файле 2.20. HomeController. cs using using using using RsvpForm из папки Controllers System; Microsoft.AspNetCore.Mvc; Partyinvites.Models; System.Linq; namespace Partyinvites.Controllers class HomeController : Controller ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" return View("MyView"); puЫic puЬlic "Good Afternoon"; [HttpGet] ViewResul t RsvpForm {) { return View (); puЬlic [HttpPost] ViewResult RsvpForm{GuestResponse guestResponse) puЫic if (ModelState.IsValid) { Repository.AddResponse(guestResponse); return View("Thanks", guestResponse); else { 11 Обнаружена ошибка провер1СИ достоверности. return View () ; ViewResult ListResponses() { return View(Repository.Responses.Where(r => r.WillAttend == true) ); puЫic Базовый класс Controller предоставляет свойство по имени ModelState. ко­ торое сообщает информацию о преобразовании данных НТГР-запроса в объекты С#. Если свойство ModelState. IsValid возвращает true. то известно. что инфраструк­ тура МVС сумела удовлетворить ограничения проверки достоверности, указанные че­ рез атрибуты для класса GuestResponse. Когда такое происходит, визуализируется Thanks, как делалось до того. Если свойство ModelState. IsValid возвращает false, то известно, что есть ошибки проверки достоверности. Возвращаемый свойством ModelState объект пре­ представление доставляет подробные сведения о каждой возникшей проблеме, но нам нет нужды погружаться на такой уровень деталей. поскольку мы можем полагаться на удобное средство, которое автоматизирует процесс указания пользователю на необходимость решения любых проблем, вызывая метод View () без параметров. Глава 2. Ваше nервое nриложение MVC Когда МVС визуализирует представление, механизм Razor 61 имеет доступ к деталям любых ошибок проверки достоверности, связанных с запросом, и вспомогательные функции дескрипторов могут обращаться к этим деталям, чтобы отображать сообще­ ния об ошибках пользователю. В листинге ставления RsvpForm. csh tml 2.21 приведено содержимое файла пред­ с добавленными атрибутами вспомогательных функций дескрипторов для проверки достоверности. Листинг 2.21. Добавление сводки по проверке достоверности в файле RsvpForm.cshtml из папки Views/Home @model Partyinvites.Models.GuestResponse @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RsvpForm</title> </head> <body> <form asp-action="RsvpForm" method="post"> <div asp-validation-suпunary="All"X/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> </р> <button type="submit">Submit RSVP</button> </form> </body> </html> Атрибут asp-validation-summary применяется к элементу div и отобра­ жает список ошибок проверки достоверности при визуализации представления. Значение для атрибута asp-validation-summary берется из перечисления по име- 62 ни Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Valida t i onSummary, которое указывает типы ошибок проверки достоверности , помещаемые в сводку. Мы указали All, что является хорошей отправной 27 будут описаны другие значения. точкой для большинства приложений, а в главе Чтобы взглянуть, как работает сводка по проверке достоверности. запустим при­ ложение, заполним поле сводка с ошибками (рис. Name и 2.19). отправим форму, не вводя другие данные. Появится • Please enter your email address • Please enter your phone number • Please specify \\·hether you'll attend -·--·-- Your name: Q Joe - ] Your eniail: [ - -~--~ -J - --- ---- Your phone: f - - - - -----J j Submlt RSVP j Рис. Метод действия 2.19. Отображение ошибок проверки достоверности RsvpForm () не визуализирует представление Th a nks до тех пор, пока не будут удовлетворены все ограничения проверки достоверности, примененные к классу Gue st Resp onse . Обратите внимание . что введенные в поле были сохранены и отображены снова при визуализации механизмом Name данные Razor представ­ ления со сводкой проверки достоверности. Это еще одно преимущество привязки мо­ дели, и оно упрощает работу с данными формы. На заметку! Если вы работали с ASP.NET Web Forms , то знаете, что в Web Forms поддержива­ ется концепция серверных элементов управления, которые сохраняют состояние, сериа­ лизируя значения в скрытое поле формы по имени_ VIEW STATE. Привязка модели MVC не имеет никакого отношения к концепциям серверных элементов управления, обратным отправкам или состоянию представления , принятым в Web Forms. Инфраструктура MVC не внедряет скрытое поле_ VIEWSTATE в визуализированные НТМL-страницы. Взамен она включает данные, устанавливая атрибуты value элементов управления inpu t. подсветка полей с недопустимыми значениями Атрибуты вспомогательных функций дескрипторов, которые ассоциируют свойс­ тва модели с элементами, обладают удобным средством , которое можно использовать Глава 2. Ваше первое приложен и е MVC 63 в сочетании с привязкой модели. Когда свой ство класса модели не проходит проверку достоверности, атрибуты вспомогательных функций дескрипторов будут генерировать несколько отличающуюся НТМL-разметку. Вот элемент для поля Phone i n put, ноторый генерируется при отсутствии ошибок проверки достоверности: <input type=" te xt" data- v a l =" tr ue" data- v a l -r e quired= " Pl e a se e nt e r y o u r phone numb e r " i d = " Pho ne " name = " Pho ne" va lu e = " " > Для сравнения ниже показан тот же НТМL-элемент после то го, как пользователь отправил форму , не введя данные в текстовое пол е (что является ошибной проверни достоверности, поскольку мы применили к свойству атрибут проверки Ph o ne класса Gu e s tRe sp o n se Required): <i npu t t.yp e = " t e x t " class=" input-validation-error" da ta- va l= "true " d at a - v al -r e q uir e d = "Ple a s e en ter y o ur p ho ne number" i.d= "Pho ne " n a me= " Phon e " v a lue= "" > Отличие выделено полужирным: атрибут вспомогательной функции дескриптора a sp -for добавил к элементу input класс по имени i n pu t-validati on -e r ror. Мы можем воспользоваться такой возможностью, создав таблицу стилей , которая содер ­ жит стили CSS для этого класса и другие стили , применяемые различными атрибута­ ми вспомогательных функций дескрипторов. По принято му в проектах MVC мое кли ентам. помещается в папку согла шени ю статическое с одержи м ое, доставляе­ wwwroot , содержимого. так что таблицы стилей JavaScript Для ке в папке создания wwwr o ot /c s s wwwroot / j s таблицы в онне CSS подпапки которой организованы по типу находятся в папке www r oot / cs s , файлы и т. д. стилей щелкнем Solution Explorer правой кнопкой мыши на пап ­ и выберем в нонтекстном меню пункт Addc::>New ltem (Добавить q Н овый элемент). В открывш е мся диалогово м окне Add New ltem (Добавле ние нового эл е м е нта) перейдем в раздел ASP.NET CoreqWebq Content (ASP.NET Соrеq Веб q Содержимо е) и вы бер ем ш аблон Style Sheet (Таблица стилей) из списка шаблонов (рис. 2.20). Add Now ltem • Partylnvites • . ASP.NEТ Соге Code General • Web ASP.NEТ General Script< Cont•nt ~ ? • Sortby:~----~ · I ~~: ::.=: ,; lnstalied Г-.. c'.)J HTML Page ASP.NET Core ~ Style Sh.,.t ASP.NET Core ~ LESS Style Sheet ASP.NET Core И:Ш~ SCSS Style Sheet (SASS) ASP.NEТ Core Search (Ctrl• E) А cascading style sheet (CSS) used for rich style definitions НТМL styles.css Рис. 2.20. Создание таблицы стиле й Р· Туре: ASP.NEТ Core Online Nam~ Х CSS Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 64 Совет. Когда для проекта используется шаблон среда Visual Studio создает файл Web site.css Applicatioп (Model-View-Coпtroller), в папке wwwroot/css. В текущей главе данный файл не задействован. Назначим файлу имя styles. css, щелкнем на кнопке Add (Добавить), чтобы со­ здать файл таблицы стилей, и приведем его содержимое к виду, показанному в лис­ тинге 2.22. Листинг 2.22. Содержимое файла s tyles. css .field-validation-error .field-validation-valid .input-validation-error .validation-suпunary-errors .validation-suпunary-valid из папки wwwroot/ css color: #fOO; ) display: none; border: lpx solid #fOO; background-color: #fee; { font-weight: bold; color: #fOO; ) { display: none; ) Чтобы применить эту таблицу стилей. мы добавили элемент представления Листинг 2.23. RsvpForm (листинг link в раздел head 2.23). Применение таблицы стилей в файле RsvpForm. cshtml <head> <meta name="viewport" content="width=device-wid th" /> <title>RsvpForm</title> <link rel="stylesheet" href="/css/styles.css" /> </head> Элемент link использует атрибут href для указания местоположения таблицы wwwroot в URL опущена. Стандартная кон­ стилей. Обратите внимание, что папка фигурация ASP.NET включает поддержку обслуживания статического содержимого, такого как изображения, таблицы стилей и файлы чески отображает запросы на папку Процесс конфигурации рассматривается в главе CSS wwwroot. JavaScript, которая автомати­ ASP.NET и MVC 14. Совет. Для работы с таблицами стилей предусмотрена специальная вспомогательная функ­ ция дескриптора, которая может оказаться полезной при наличии многочисленных фай­ лов, требующих управления. Подробности ищите в главе 25. Благодаря применению таблицы стилей в случае отправки данных. которые вызы­ вают ошибки проверки достоверности, отображается визуально более ясные сообще­ ния об ошибках (рис. 2.21). Глава 2. Ваше первое приложе ние MVC 65 • Please enter your email address • Please enter your pbone nuшber • Please specif)· \Yhether you 'll attend l Your name: ~ое . \\.ill you attend? 1Choose an option • ] J ГsUЬmit RSVP Рис. 2.21. Автоматическая подсветка полей с ошибками проверки достоверности Стилизация содержимого Все цели приложения, касающиеся функциональности, достигнуты , но его общий вид оставляет желать лучшего. Когда вы создаете проект с использованием шаблона Web Applicatioп (Model-View-Coпtroller). как в рассматриваемом примере. среда Visual Studio устанавливает несколько распространенных пакетов для разработки на сто · роне клиента. Хотя я не являюсь сторонником применения шаблонов проектов. мне нравятся выбранные вается Bootstrap Microsoft библиотеки клиентской стороны. Одна из них назы­ и представляет собой удобную инфраструктуру CSS, первоначально разработанную в ТWitter, которая постепенно превратилась в крупный проект с от­ крытым кодом и стала главной опорой разработки веб-приложений. Bootstrap 3, но версия Microsoft могут принять решение обно­ Web Applicatioп, в последующих выпус­ На заметку! На момент написания книги текущей версией была Bootstrap 4 находилась на стадии разработки. В вить версию шаблоном ках Bootstrap, используемую Visual Studio, что может привести к отображению содержимого по-другому. В других главах книги это не должно стать проблемой, т.к. там будет показано, каким образом явно указывать версию пакета, чтобы получить ожидаемые результаты. Стилизация начального представления Базовые средства Bootst.rap работают за счет применения классов к элементам , которые соответствуют селекторам CSS, определенным внутри добавленных в папку wwwroot/liЬ/bootstrap файлов. Подробную информацию о классах. определенных в библиотеке в листинге ставлении Bootstrap, можно получить на веб-сайте http: //ge tboots t rap.c om, а 2.24 демонстрируется использование нескольких базовых стилей в пред­ MyVi e w. cshtml . Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 66 Листинг Добавление классов 2.24. из папки Bootstrap Views/Home в файле МyView. cshtml @{ Layout nul l ; = < 1 DOCTYPE html> <h tml > <head> <meta name= " view port " content="w id th=device -w idt h" /> <title >Index</t itle > <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 invi ted</h4> <а class="Ьtn Ьtn-primary" asp-action="RsvpForm">RSVP Now</a> </d1v> </body> </html> В разметку был добавлен элемент bootstrap. css ты из папки wwwroo t / l ink. атрибут href которого загружает файл l iЬ/ bootstrap / d ist/css. По соглашению паке ­ CSS и JavaScгipt от независимых поставщиков устанавливаются lib. и в главе 6 в папку www root / будет описан инструмент, предназначенный для управления такими пакетами. После импортирования таблиц стилей Bootstrap осталось стилизовать элементы. Рассматриваемый пример прост, поэтому требует использования лишь небольшого числа классов Класс CSS из Bootstrap: text- cen ter, text-center Ьtn и Ьtn -prima ry . центрирует содержимое элемента и его дочерних элементов. Класс Ьtn стилизует элемент butt on, input или а в виде симпатичной кнопки. а класс Ьtn-primary указывает диапазон цветов для кнопки. Запустив приложение. можно увидеть результат. показанный на рис . ~ С 2.22. <D localhost:S7628 We're going to have an exciting party! And you are invited 1 ;Н11!!.\!Щ Рис. 2.22. Стилизация представления Глава 2. Ваше первое приложение MVC Совершенно очевидно, что я - 67 не веб-дизайнер. На самом деле, будучи еще ребен­ ком, я был освобожден от уроков рисования по причине полного отсутствия таланта. Это произвело благоприятный эффект в виде того, что я стал уделять больше време­ ни урокам математики, но вместе с тем мои художественные навыки не развивались примерно с десятилетнего возраста. Для реальных проектов я обратился бы к помощи профессионального дизайнера содержимого, но в настоящем примере я собираюсь де­ лать все самостоятельно и применять Bootstrap с максимально возможной сдержан­ ностью и согласованностью, на какую только способен. Стилизация представления В библиотеке Bootstrap RsvpForm определены классы, которые могут использоваться для стилизации форм. Я не планирую вдаваться в особые детали, но в листинге 2.25 по­ казано, как были применены эти классы. Листинг 2.25. Добавление классов из папки Bootstrap Views/Home в файле RsvpForm. cshtml @model Partyinvites.Models.GuestResponse @{ Layout = null; < 1 DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RsvpForm</title> <link rel="stylesheet" href=" /css/styles. css" /> <link rel="stylesheet" href="/lib/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-1" asp-action="RsvpForm" method="post"> <div asp-validation-summary="All"></div> <div class="form-group"> <laЬel asp-for="Name">Your name:</laЬel> <input class="form-control" asp-for="Name" /> </div> <div class="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" aзp-for="WillAttend"> 68 Часть 1. Введение в инфраструктуру ASP. NET Core MVC 2 <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="suЬmit"> SuЬmit RSVP </button> </div> </form> </div> </div> </body> </html> Классы Bootstrap в данном примере создают заголовок, просто чтобы придать компоновке структурированность. Для стилизации формы используется класс group, который стилизует элемент, содержащий label и связанный или select. Результаты стилизации можно видеть на рис. 2.23. С 1 Ф loca~ho~:57628/Home/Rsvp_:o~m RSVP Yourname: Youremail: Yourphone: Will you attend? Choose an option SuЬmitRSVP Рис. 2.23. Стилизация представления RsvpForm элемент f orminpu t Глава 2. Ваш е первое приложе н ие MVC Стилизация представления Thanks Следующим стилизуемым представлением является ге 2.26 показано, 69 как это делается с применением классов Thanks. c shtml : в CSS, подобных тем , листин ­ которые ис пользовались для других представлений. Чтобы облегчить управление приложени­ ем, имеет смысл везде, где только возм ожно, избегать дублирования кода и разметки . Инфраструктура MVC предлагает несколько средств, помогающих сократить дубли­ рование, которые рассматрива ются в последующих гл авах. К таким средствам отно­ сятся компоновки Razor (глава 5). 22). частичные представления (глава 21) и компоненты представлений (глава Листинг 2.26. Применение классов из папки Bootstrap Views/Home в файле Thanks. cshtml @model Par t y i nv it es .Mode l s . Gues t Re sp onse @{ Layou t = nul l ; < ! DOCTYPE h tml> <html> <head> <meta name=" vi ewport" con te nt ="w idth=device - width" /> <ti t l e>T han ks< / t itl e> <link rel="stylesheet" href="/lib/bootstrap/dist/css/Ьootstrap . css" /> </head> <body class="text-center"> < р> <hl>Tha n k you , @Mode l. Na me ! </ hl> @if (Mode l . WillAttend == t rue ) { @:It's g r e a t t hat you ' re coming. The dri nks are a lready in the f r i dge! e l se { @: Sorry t o he a r that you can 't ma ke i t , but thanks for lett i ng us know. < / р> Click <а class="nav-link" asp-action="ListResponses">here</a> to see who is coming. </ bod y> </htпil > Н а ри с . 2.24 пока зан результат стил изации. Thanks С l х !ф localhost.57628/Ho~/RsvpFom1 Thank you, Joe! lt's great that you're coming. The drinks are alreacly in the fridge! Click here to see who is coming. Рис. 2.24. Стилизация представления Tha n ks Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 70 Стилизация представления Lis tResponses Последним мы стилизуем представление ListResponses. которое отображает список участников вечеринки. Стилизация содержимого следует тому же базовому подходу, который применялся в отношении всех стилей Bootstгap (листинг 2.27). Листинг 2.27. Добавление классов Bootstrap в файле ListResponses. cshtml из папки @model Views/Home IEnumeraЫe<Partyinvites.Models.GuestResponse> @{ Layout = null; < 1 DOCTYPE htшl> <htшl> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" /> <title>Responses</title> </head> <body> <div class="panel-body"> <h2>Here is the list of people attending the party</h2> <tаЫе class="taЫe taЫe-sm taЫe-striped taЫe-bordered"> <thead> <tr> <th>Name</th> <th>Eшail</th> <th>Phone</th> </tr> </thead> <tbody> @foreach (Partyinvites.Models.GuestResponse r in Model) { <tr> <td>@r.Naшe</td> <td>@r.Eшail</td> <td>@r.Phone</td> </tr> </tbody> </tаЫе> </div> </body> </htшl> На рис. 2.25 показано, как теперь выглядит таблица участников вечеринки. Добавление стилей к данному представлению завершает пример приложения, кото­ рое теперь достигло всех целей разработки и имеет намного более совершенный вне­ шний вид. Глава 2. Ваше первое приложение MVC (- С 71 ,-ф localhost57628/Home/l1stResponses Here is the list of people attending the party Name Email Phone Joe [email protected] 555-1234 Alice [email protected] 555-5678 ВоЬ ЬoЬ@example.com 255-2345 Рис. 2.25. Стилизация представления L i stResponses Резюме MVC, который использовался для построения MVC, что позволило получить первое представле­ ние об архитектуре ASP.NET Core MVC и применяемом подходе. Некоторые основные средства (включая синтаксис Razor. маршрутизацию и тестирование) не рассматри­ В главе был создан новый проект простого приложения ввода данных вались , но мы вернемся к данным темам в последующих главах . В следующей главе MVC , которые ASP.NET Core MVC. будет описан ы паттерны проектирования тивной р азработки с помощью формируют основу эффек­ ГЛАВА 3 Паттерн, проекты и соглашения МVС режде чем приступать к углубленному изучению деталей инфраструктуры п ASP.NET Core MVC, необходимо освоить паттерн проектирования МVС. лежа­ щие в его основе концепции и способ, которым они транслируются в проекты ASP.NEТ Core MVC. Возможно, вы уже знакомы с некоторыми идеями и соглашениями, обсуж­ даемыми в настоящей главе, особенно если вам приходилось заниматься разработкой сложных приложений ASP.NET или С#. Если это не так, тогда внимательно читайте главу. Хорошее понимание того, что положено в основу MVC, может помочь увязать функциональные возможности инфраструктуры с контекстом материала, излагаемо­ го в оставшихся главах книги. История создания MVC Термин модель-представление-контроллер (model-view-controller - MVC) был в Smalltalk в Xerox PARC. употреблении с конца 1970-х годов и происходит из проекта где он был задуман как способ организации ряда ранних приложений с графичес­ ким пользовательским интерфейсом. Некоторые нюансы первоначального паттерна MVC были связаны с концепциями, специфичными для Smalltalk, такими как экраны и инструменты, но более широкие понятия по-прежнему применимы к приложени­ ям - и особенно хорошо они подходят для веб-приложений. Особенности паперна MVC Если оперировать высокоуровневыми понятиями. то паттерн МVС означает, что приложение • MVC Модели, будет разделено. по крайней мере, на три части. содержащие или представляющие данные, с которыми работают пользователи. • Представления. используемые для визуализации некоторой части модели в виде пользовательского интерфейса. • Контроллеры, которые обрабатывают входящие запросы. выполняют операции с моделью и выбирают представления для визуализации пользователю. Глава 3. Паттерн, проекты и соглашения MVC 73 Каждая порция архитектуры МVС четко определена и самодостаточна: такое по­ ложение вещей называют разделением обязанностей. Логика, которая манипулиру­ ет данными в модели, содержится только в модели. Логика, отображающая данные, присутствует только в представлении. Код, который обрабатывает пользовательские запросы и ввод, находится только в контроллере. Благодаря ясному разделению меж­ ду частями приложение будет легче сопровождать и расширять на протяжении его времени существования вне зависимости от того, насколько большим оно станет. Понятие моделей Модели (М в MVC) содержат данные, с которыми работают пользователи. Существуют два обширных типа моделей: модели представлений, которые выража­ ют сами данные, передаваемые из контроллера в представление, и модели предмет­ ной области, которые содержат данные в предметной области наряду с операциями, трансформациями и правилами для создания, хранения и манипулирования данны­ ми, вместе называемыми логикой моделей. Модели - это определение "вселенной", в которой функционирует приложение. Например, в банковском приложении модель представляет все аспекты банковской деятельности, поддерживаемые приложением, такие как расчетные счета, главная бухгалтерская книга и кредитные лимиты для клиентов, равно как и операции, ко­ торые могут применяться для манипулирования данными в модели, подобные вне­ сению денежных средств и списанию их со счетов. Модель отвечает также за сохра­ нение общего состояния и целостности данных - например, удостоверяясь, что все транзакции внесены в главную книгу, а клиент не снимает со счета больше денежных средств, чем имеет на то право, или больше, чем находится в распоряжении самого банка. Для каждого компонента в паттерне MVC будет описано, что он должен включать, а что не должен. Модель в приложении, построенном с использованием паттерна МVС, должна: • содержать данные предметной области: • содержать логику для создания, управления и модификации данных предмет­ ной области: • предоставлять чистый АРI-интерфейс, который открывает доступ к данным мо­ дели и операциям с ними. Модель не должна: • показывать детали того, как осуществляется получение или управление дан­ ными модели (другими словами, подробности механизма хранения данных не должны быть видны контроллерам и представлениям); • содержать логику, которая трансформирует модель на основе взаимодействия с пользователем (поскольку это работа контроллера): • содержать логику для отображения данных пользователю (т.к. это работа представления). Преимущества обеспечения изоляции модели от контроллера и представлений за­ ключаются в том, что вы можете гораздо легче тестировать логику (модульное тести­ рование описано в главе 7) и проще расширять и сопровождать приложение в целом. 74 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Совет. Многих разработчиков, только приступивших к ознакомлению с паттерном MVC, при­ водит в замешательство идея помещения логики в модель данных из-за их уверенности в том, что целью паттерна цель паперна MVC - MVC является отделение данных от логики. Это заблуждение: разделение приложения на три функциональных области, каждая из которых может содержать и логику, и данные. Цель не в том, чтобы устранить логику из модели. Наоборот, цель в том, чтобы гарантировать наличие в модели только логики, предназначенной для создания и управления данными модели. Понятие контроллеров Контроллеры являются "соединительной тканью" паттерна MVC, исполняя роль каналов между моделью данных и представлениями. Контроллеры определяют дейс­ твия, предоставляющие бизнес-логику, которая оперирует на модели данных и обес­ печивает представления данными, подлежащими отображению для пользователя. Контроллер, построенный с применением паттерна МVС, дол:ж:ен: • содержать действия, требующиеся для обновления модели на основе взаимо­ действия с пользователем. Контроллер не должен: • содержать логику, которая управляет внешним видом данных (это работа представления); • содержать логику, которая управляет постоянством данных (это работа модели). Понятие представлений Представления содержат логику. которая требуется для отображения данных поль­ зователю или для сбора данных от пользователя, так что они могут быть обработаны каким-то действием контроллера. Представления дол:ж:ны: • содержать логику и разметку. необходимые для показа данных пользователю. Представления не должны: • • содержать сложную логику (ее лучше поместить в контроллер); содержать логику, которая создает, сохраняет или манипулирует моделью пред­ метной области. Представления могут содержать логику, но она должна быть простой и использо­ ваться умеренно. Помещение в представление чего угодно кроме вызовов простейших методов или несложных выражений затрудняет тестирование и сопровождение при­ ложения в целом. Реализация паттерна MVC в ASP.NET Core ASP.NET Core MVC адаптиру­ ASP.NET и С#. В инфраструктуре Как подразумевает само название, реализация ет абстрактный паттерн ASP.NET Core MVC MVC к миру разработки контроллеры - это классы С#, обычно производные от класса Microsoft. AspNetCore. Mvc. Con trol ler. Каждый открытый метод в производном от Controller классе является методом действия, который ассоциирован с каким­ то URL. Когда запрос отправляется по URL, связанному с методом действия, опера­ торы в данном методе действия выполняются, чтобы провести некоторую операцию над моделью предметной области и затем выбрать представление для отображения клиенту. Взаимодействия между контроллером, моделью и представлением проил­ люстрированы на рис. 3.1. Глава 3. Паттерн , проекты и соглашения MVC Запрос НТТР _ ._ Постоянство - - - - -- - -- - ---1..i Контроллер ....__ _ _ ___. Модель представления Рис. В инфраструктуре вестный как 3.1. (обычно с помощью Модель Представление ~-----1 Ответ 75 реляционной - - базы данных) .___ _ __, Взаимодействия в приложении ASP.NET Core MVC MVC применяется механизм визуал изации, из ­ который является компонентом, ответственным за обработку Razor, представления для генерации ответа браузеру. Представления Razor - это НТМL­ шаблоны , содержащие логику С# , которая используется для обработки данных мо ­ дели с целью генерации динамического содержимого , реагирующего на изменения в Razor рассматрив ается в главе 5. ASP.NET MVC не налагает никаких огра ничений модели. Работа механизма Инфра структура на реализацию модели предметной области. Вы можете создать модель с применением обычных объектов С# и реализовать постоянство с использованием любых баз данных, инф­ раструктур объектно-реляционного отображения или других инструментов работы с данными. поддерживаемых в .NET. Понятие одностраничных приложений Исторически сложилось так, что при разработке веб-приложений браузеры обычно тракто­ вались как простое устройство отображения для визуализации НТМL-разметки и реагирова ­ ния на щелчки кнопками мыши. Такой стиль веб-приложений называется полным циклом об­ мена. Каждый раз , когда пользователь щелкает на ссылке , приложению ASP.NET Core MVC посылается НТТР-запрос. Внутри приложения контроллер выбирает представление, которое визуализируется механизмом Razor и отправляется обратно браузеру, так что пользователю может быть отображена новая НТМL-страница . Вся логика , данные и состояние находятся Core MVC, что упрощает разработку и означает отсутствие необходи­ на сервере ASP.NEТ мости уделять много внимания браузеру, кроме как проверять, поддерживает ли он возмож­ ности HTML, реализованные в представлениях Razoг. По контрасту с ними одностраничные приложения включают браузер в платформу прило­ жений. Сервер несет ответственность за управление данными приложения , в то время как код JavaScript, выполняющийся в браузере, запрашивает эти данные, отображает их поль­ зователю и реагирует на пользовательское взаимодействие . В одностраничном приложе­ нии обязанности, касающиеся моделей, представлений и контроллеров, разделяются меж­ ду браузером и сервером. Вместо отправки полных НТМL-страниц браузеру часть ASP.NET Core MVC приложения обеспечивает доступ к данным приложения, которые запрашиваются и отображаются инфраструктурой JavaScript, такой как Angular или React. Одностраничные приложения могут быть более отзывчивыми, чем приложения с полным циклом обмена, но их сложнее создавать, а эффективная разработка таких приложений JavaScript, сложность которых не должна недо­ 20 будет показано , как можно использовать инфраструктуру ASP.NET требует навыков работы с языками С# и оцениваться . В главе Core MVC для предоставления данных в одностраничном приложении, но процесс разра­ ботки приложений подобного рода не демонстрируется, поскольку он сам по себе является JavaScript я отдаю предпочтение Angular, которая Pro Angular. При желании задействовать Angular с ASP.NET Core MVC Essentia/ Angular for ASP.NET Саге MVC. отдельной темой. Среди инфраструктур описана в моей книге почитайте мою книгу Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 76 Сравнение MVC Разумеется, с другими паттернами MVC - далеко не единственный архитектурный паттерн программно­ го обеспечения. Есть много других паттернов. и некоторые из них чрезвычайно по­ пулярны или, по крайней мере , были таковыми. О паттерне MVC можно узнать много интересного, сравнивая его с альтернативами. В последующих разделах кратко опи­ саны разные подходы к структурированию приложения и приведено их сравнение с MVC. Одни паттерны будут близкими вариациями на тему MVC, тогда как другие со­ вершенно отличаться. Я не утверждаю. что MVC - идеальный паттерн во всех ситуациях . Я сторонник выбора такого подхода, который лучше всего решает имеющуюся задачу . Как вы вско­ ре увидите , есть ситуации, когда некоторые конкурирующие паттерны в равной сте­ пени полезны или лучше MVC. Я призываю делать осознанный и взвешенный выбор паттерна. Сам факт чтения вами этой книги наводит на мысль, что в определенной мере вы уже склонны применять паттерн MVC. но всегда полезно сохранять макси­ мально широкую точку зрения . Паттерн "Интеллектуальный пользовательский интерфейс" Один из наиболее распространенных паттернов проектирования известен как "Интеллектуальный пользовательский интерфейс" (Smart UI). Большинству про­ граммистов приходилось создавать приложение с интеллектуальным пользователь­ ским интерфейсом на том или ином этапе своей профессиональной деятельности меня это определенно касается. Если вы использовали Web Forms, Windows Forms или ASP.NET то сказанное относится и к вам. При построении приложения с интеллектуальным пользовательским интерфей­ сом разработчики конструируют интерфейс, часто перетаскивая набор компонентов либо элементов управления на поверхность проектирования или холст. Элементы управления сообщают о взаимодействии с пользователем, инициируя события для щелчков кнопками мыши, нажатий клавиш на клавиатуре, перемещений курсора и т.д. Разработчик добавляет код реакции на такие события в набор обработчиков событий, которые являются небольшими блоками кода. вызываемыми при выдаче специфического события в определенном компоненте. В конечном итоге получается монолитное приложение, показанное на рис . Запрос __.., Интеллектуальный 3.2. __ _. пользовательский Отв ет Рис. 3.2. интерфейс Постоянство (обычно с помощью реляционной - - базы данных) Паттерн "Интеллектуальный пользовательский интерфейс" Код, который поддерживает пользовательский интерфейс и реализует бизнес-ло­ гику, перемешан между собой без какого-либо разделения обязанностей. Код. кото­ рый определяет приемлемые значения для вводимых данных и запрашивает данные или модифицирует, например, расчетный счет пользователя, оказывается размещен­ ным в небольших фрагментах. связанных друг с другом в предполагаемом порядке поступления событий. Глава 3. Паттерн, проекты и соглашения MVC 77 Интеллектуальные пользовательские интерфейсы идеальны для простых проектов, т.к. позволяют добиться неплохих результатов достаточно быстро (по сравнению с раз­ работкой МVС, которая. как показано в главе 8, требует определенных начальных затрат, прежде чем будут доставлены результаты). Интеллектуальные пользовательские интер­ фейсы также подходят для построения прототипов пользовательских интерфейсов. Их инструменты визуального конструирования могут оказаться по-кастоящему удобными. и если вы обсуждаете с заказчиком требования к внешнему виду и потоку пользователь­ ского интерфейса, то инструмент интеллектуального пользовательского интерфейса мо­ жет быть быстрым и удобным способом для генерации и проверки различных идей. Самый крупный недостаток интеллектуальных пользовательских интерфейсов связан с тем, что их трудно сопровождать и расширять. Смешивание кода модели предметной области и бизнес-логики с кодом пользовательского интерфейса приво­ дит к дублированию кода, когда один и тот же фрагмент бизнес-логики копируется и вставляется для поддержки вновь добавленного компонента. Нахождение всех дуб­ лированных фрагментов и применение к ним исправления может быть непростой за­ дачей. Добавление новой функции может оказаться практически невозможным без нарушения работы каких-то существующих функций. Тестирование приложения с интеллектуальным пользовательским интерфейсом также может быть затруднено. Единственный способ тестирования - эмуляция взаимодействия с пользователем, что является далеким от идеала и трудно реализуемым фундаментом для обеспечения полного покрытия тестами. В мире MVC интеллектуальный пользовательский интерфейс часто называют актuпаттерком, т.е. чем-то таким. чего следует избегать любой ценой. Такая ан­ типатия возникает (по крайней мере, частично) оттого, что к инфраструктуре MVC обращаются в поисках альтернативы после множества не слишком успешных попы­ ток разработки и сопровождения приложений с интеллектуальным пользовательским интерфейсом, которые попросту вышли из-под контроля. Тем не менее, полный отказ от паттерна "Интеллектуальный пользовательский интерфейс" будет заблуждением. Далеко не все в нем настолько плохо, и с данным подходом связаны также положительные аспекты. Приложения с интеллектуальным пользовательским интерфейсом быстро и легко разрабатывать. Производители ком­ понентов и инструментов проектирования приложили немало усилий, чтобы сделать процесс разработки приятным занятием, поэтому даже совершенно неопытный про­ граммист за считанные часы может получить профессионально выглядящий и доста­ точно функциональный результат. Наибольшая слабость приложений с интеллектуальным пользовательским интер­ фейсом - низкое удобство сопровождения - не проявляется при мелких объемах разработки. Если вы создаете несложный инструмент для небольшой аудитории, то приложение с интеллектуальным пользовательским интерфейсом может оказаться идеальным решением. Просто отсутствует дополнительная сложность, присущая раз­ работке приложения MVC. Архитектура "модель -представление" В приложении с интеллектуальным пользовательским интерфейсом проблемы сопро­ вождения обычно возникают в области бизнес-логики, которая настолько рассеяна по приложению, что внесение изменений или добавление новых функций становится му­ чительным процессом. Улучшить ситуацию помогает архитектура "модель-представ­ лекuе", которая выносит бизнес-логику в отдельную модель предметной области. Все данные, процессы и правила концентрируются в одной части приложения (рис. 3.3). Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 78 Запрос _ ._ Пользовательский -----~ Модель интерфейс (представление) Ответ Рис. 3.3. -- Постоянство (обычно с помощью реляционной базы данных) Архитектура "модель-представление" Архитектура "модель-представление" может быть значительным усовершенствова­ нием по сравнению с монолитным интеллектуальным пользовательским интерфейсом (скажем. ее гораздо легче сопровождать). но возникают две проблемы . Первая: пос­ кольку пользовательский интерфейс и модель предметной области тесно переплете­ ны, модульное тестирование любой из упомянутых частей может оказаться затрудни­ тельным. Вторая проблема проистекает из практики. а не из определения паттерна . Обычно модель содержит большой объем кода доступа к данным (не обязательно. но в большинстве случаев). а это означает, что модель данных содержит не только бизнес­ данные. операции и правила. Классическая трехуровневая архитектура Чтобы решить проблемы . присущие архитектуре "м одель-представле ние", трех­ звенная или трехуровневая архитектура отделяет код реализации постоянства от модели предметной области и помещает его в новый компонент. который называется уровнем доступа к данным на рис. (data access Iауег - DAL). Сказанное проиллюстрировано 3.4. Постоянство Запрос Пользовательский 1-----~ Модель интерфейс Ответ (представление) Рис. 3.4. - Уровень -•(обычно доступа с помощью к данным реляционной - - базы данных) Трехуровневая архитектура Трехуровневая архитектура является наиболее широко используемым паттерном в бизнес-приложениях. Она не устанавливает никаких ограничений на с пособ реализа ­ ции пользовательского интерфейса и обеспечивает хорошее разделение обязанностей . не будучи излишне сложной. Кроме того. ценой некоторых усилий уровень DAL мо­ жет быть создан так. чтобы модульное тестирование проводилось относительно легко . Между классическим трехуровневым приложением и паттерном MVC можно заметить очевидные сходства. Разница в том. что когда уровень пользовательского интерфей­ са напрямую связан с инфраструктурой графического пользовательского интерфейса типа "щелчок-событие" (такой как Windows Foгms или ASP.NET Web Foгms) , то выпол­ нение автоматизированных модульных тестов становится практически невозможным . А поскольку часть пользовательского интерфейса трехуровневого приложения может быть сложной, останется много кода . не подвергавшегося серьезному тестированию. В худшем сценарии отсутствие принудительных ограничений , накладываемых трехуровневой архитектурой на уровень пользовательского интерфейса. означает. что многие такие приложения в итоге оказываются тонко замаскиров анными при - Глава 3. Паперн, проекты и соглашения MVC 79 ложениями с интеллектуальным пользовательским интерфейсом без какого-либо ре­ ального разделения обязанностей. Это ведет к наихудшему конечному результату: не поддерживающему тестирование и трудному в сопровождении приложению. которое к тому же чрезмерно сложно. Разновидности MVC Основные принципы проектирования приложений MVC были уже описаны, осо­ бенно те из них. которые применимы к реализации ASP.NEТ лизации интерпретируют аспекты паттерна MVC Core MVC. Другие реа­ иначе. дополняя, подстраивая или как-то еще адаптируя его для соответствия области охвата и целям своих проектов. В последующих разделах мы кратко рассмотрим две наиболее распространенные ва­ риации на тему MVC. Понимание этих разновидностей не ASP.NET Core МVС; данная информация в случае работы с имеет особого значения включена ради полноты картины, потому что такие термины будут употребляться в большинстве обсуждений паттернов проектирования ПО. Паттерн "модель-представление-презентатор" Паттерн "модель-представление-презентатор" ляется разновидностью MVC (model-view-presenter - MVP) яв­ и разработан для того. чтобы облегчить согласование с поддерживающими состояние платформами графического пользовательского интер­ фейса. такими как Windows Forms или ASP.NET Web Forms. Это достойная попытка из­ влечь лучшее из паттерна интеллектуального пользовательского интерфейса, избежав проблем, которые он обычно привносит. В паттерне МVР презентатор имеет те же обязанности, что и контроллер МVС. но он также более непосредственно связан с представлением, сохраняющим информа­ цию о состоянии, напрямую управляя значениями, которые отображаются в компо­ нентах пользовательского интерфейса в соответствии с вводом и действиями пользо­ вателя. Существуют две реализации паттерна • MVP. Реализация пассивного представления. в которой представление не содер­ жит никакой логики. Такое представление служит контейнером для элементов управления пользовательского интерфейса, которыми напрямую управляет презентатор. • Реализация координирующего контроллера, в которой представление может от­ вечать за определенные элементы логики презентации. такие как привязка дан­ ных. и получать ссылку на источник данных от моделей предметной области. Отличие между указанными двумя реализациями касается уровня интеллекту­ альности представления. В любом случае презентатор отделен от инфраструктуры графического пользовательского интерфейса, что делает логику презентатора более простой и подходящей для модульного тестирования. Паттерн "модель-представление-модель представления" Паттерн "модель-представление-модель представления" (model-view-view model Microsoft разновидность МVС, которая используется в инфраструктуре Windows Presentation Foundation (WPF). В рамках паттерна MWM модели и представления играют те же самые роли, что и в MVC. Разница связана с присутствующей в MWM концепцией модели представления. которая является МVVM) - недавно выпущенная абстрактным обозначением пользовательского интерфейса. Как правило. модель 80 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 представления - это класс С#, который открывает доступ к свойствам для данных. подлежащих отображению в пользовательском интерфейсе. и операциям с данны­ ми. инициируемым из пользовательского интерфейса. В отличие от контроллера MVC модель представления MWM не имеет ни малейшего понятия о существова­ нии представления (или любой конкретной технологии пользовательских интерфей­ сов). Представление MWM применяет средство привязки WPF. чтобы установить двунаправленное соединение между свойствами, доступ к которым открывают эле­ менты управления в представлении (вроде пунктов раскрывающегося меню или эффекта от щелчка на кнопке), и свойствами, доступ к которым открывает модель представления. На заметку! В паттерне MVC также используется термин модель представления, но он отно­ сится к простому классу модели, предназначенному только для передачи данных из конт­ роллера в представление, как противоположность моделям предметной области, которые являются сложными представлениями данных, операций и правил. Проекты ASP.NET Core MVC При создании нового проекта ASP.NET Core MVC среда Visual Studio предлагает на выбор несколько вариантов начального содержимого, которое желательно иметь в проекте. Идея заключается в том, чтобы облегчить разработчикам-новичкам процесс обучения и применить сберегающий время передовой опыт при описании распростра­ ненных средств и задач. Я не поклонник такого подхода в отношении заготовленных проектов или кода. Намерения обычно хороши, но исполнение никогда не приводит в восторг. Одной из характеристик, которая мне больше всего нравится в и MVC, ASP.NET является высочайшая гибкость в подгонке платформы под мой стиль разра­ ботки. Процессы, классы и представления, которые создает и наполняет среда Studlo, Visual вызывают у меня чувство. что я вынужден работать в стиле кого-то другого. К тому же я нахожу содержимое и конфигурацию слишком обобщенными и прими­ тивными. чтобы приносить хоть какую-нибудь пользу. Разработчики в Microsoft не могут знать, какой вид приложения необходим, поэтому они охватывают все основы. но настолько обобщенным путем. что в итоге я просто избавляюсь от всего стандар­ тного содержимого. Моя рекомендация (всем, кто по недоразумению спросил) - начинать с пустого проекта и добавлять нужные папки, файлы и пакеты. Вы не только узнаете больше о способе работы МVС, но и будете обладать полным контролем над тем. что содержит ваше приложение. Но мои предпочтения не должны задавать тон вашей практике разработки. Вы мо­ жете счесть шаблоны более удобными, нежели я, особенно если являетесь новичком в разработке с помощью ASP.NET и еще не выработали стиль, который устраивает лич­ но вас. Вы можете также признавать шаблоны проектов полезным ресурсом и источ­ ником идей, хотя должны проявлять осмотрительность и не добавлять функциональ­ ность к приложению до того, как полностью поймете. каким образом она работает. Создание проекта При создании нового проекта ASP.NET Core предлагается выбрать вариант из це­ лого набора отправных точек, как показано на рис. 3.5. Глава 3. Паттерн , проекты и соглашения MVC .NEТCore у ~ ~ Empty w.ьдРI ASP.NEТ 1 • Core 2.0 w.ь iJ Application 81 ill.rn.!!!ш А projort template for creating an ASP.NEТ Core application with example ASP.NEТ Core MVC Vitws and Controllus. This t•mpllte con also Ье used for RESTTul о НTTPsuvi<os. Angular •• . • " 6ЭJ R•oct.js Re•ctjs 11nd R.dux 1 1Chonge Authenticotion j Authentication No AuthentiatIOn О ЕnаЫе Docker Support 05' Nndaws R.equirn Qo<tff forWindows Docker support оп also Ье enaЬl•d latu ~ ~1 Рис. Шаблон проекта Empty 3.5. Шаблоны проектов Cancel ASP.NET Core (Пустой) содержит связующие механизмы для инфраструк­ туры ASP.NET Core, но не включает библиотеки или конфигурацию. требующиеся приложению MVC. В состав шаблона проекта Web API входят инфраструктуры ASP. NET Соге и MVC, а также пример приложения, который демонстрирует получение и обработку запросов данных от клиентов с использованием контроллера АР!. который будет описан в главе Шаблон проекта 20. Web Application (Model-View-Controller) представление-контроллер)) содержит инфраструктуры (Веб-приложение (модель­ ASP.NET Соге и MVC с приме­ ром приложения, иллюстрирующим генерацию НТМL-содержимого. Шаблоны и Web Application (Model-View-Controller) Web API могут конфигурироваться с различными схе­ мами для аутентификации пользователей и авторизации их доступа к приложению . Остальные шаблоны предоставляют начальное содержимое, подходящее для ра ­ боты с инфраструктурами одностраничных приложений (Aпgular и структурой Razor Pages React) и с инфра­ (которая разрешает смешивать в одном файле код и размет­ ку. объединяя роли контроллеров и представлений, и ради простоты отказывается от ряда преимуществ модели MVC). Шаблоны проектов могут производить впечатление. что для создания определен­ ного вида приложения ASP.NEТ вы обязаны следовать специфическому пути. но дело обстоит вовсе не так. Шаблоны - это просто разные отправные точки для получе­ ния той же самой функциональности, и вы можете добавлять в проекты. созданные с помощью любого шаблона. любую желаемую функциональность. Скажем. в главе объясняется. каким образом работать с НТТР-запросами данных, а в главах 20 28-30 - как поступать с аутентификацией и авторизацией. причем все примеры будут начи­ наться с шаблона проекта Empty. Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 82 Реальная разница между шаблонами проектов связана с начальным набором библи­ отек. конфигурационных файлов, кода и содержимого. который добавляет среда Studio Visual {Empty) и при создании проекта. Существует много отличий между простейшим {Web Application (Model-View-Controller)) проектами, как можно видеть Solution Explorer после создания проектов с примене­ нием каждого шаблона . Для случая с шаблоном Web Application (Model-View-Controller) пришлось предоставить снимки окна Solution Explorer с разными открытыми папками, самым сложным на рис. 3.6, где приведено окно потому что полный список файлов не уместился бы на печатной странице. SolWon~tr Solutio11&pl0ttr ~ы- ' Э· ," 5NrthSc1utionExplo•t1(Ct1l•_;J ~ &.· 1'ф · ~~ "'il> I '""'ю.""'~" ~ ·119 ·'<" · Sol11tionЬpiortf ' ~ 5"r<h Soluuon &;p/oftr (Ctli•;} 1"""1 ·1 'э·'<" Stal(hSoiutionE>.plortr{Ctti•;) - f' ~ Solution'WtЬдppkatiol'ISecurIO'(~~~!!!l·••8'~m;!!!!•-.•=······· · ~Conм~S.М.:es Account W...ьA\)plt~tIOIOecur~ (t>Cnnnкttd~f!'I fl .~/ Dtpendtiкits t J-P1optltiei 1> .-;: ~ptnO.n<iп " ; Propcrtia 1> 8-root fl С'1 Prog.ram..::s fl С• Shrtup.U eJ 1> 1> 1::,1 8 0 0 8 , 1, rnages fO &Ь . controRм 1> Со- AccountConttolltf.CS fl cw НomtCQnt1on1r.o 1> " J> О> с- "' Stivica 1Em1ilStndtr.c1 с- t• ISm1Stndtr.t1 J> • с• Мfl119tStМcн.cs 8 Vitllf1; J> Ю' ~pstttln9JJ5on •и-;... а ЬundlкonftgJюn Pn:i91tm.n t• JndoVlewModd.n Со ~n1~in1V._мod С" Rtm-toginV-..мodt AЬoutдhtml M1n1ge .iCfl.ltfY"Ylitdltion· unoЬtruЖt li) flVkon.ko J> с• Sf'IP11swordVitwМ~.._---.---------' J> с• VeiifyPhontNumberVitwМodel.cs • С" дpplk•tionUstr,CJ J> С" [tf'OiYitwМodd..ct е AddPhoneNumЬtr.cstrt·-1---~------~ ~ CtwngeP1ssword.ciht.ml l]!ndc.цifltml @! Mtl\lgtlo9int.tshtml @!SttPщwotcl.cU!1ml ~~~.t$hlrnl Shtr1d Q ).qout.ahtml 0 0 8 C- St•lt\lp.Ct J> S9nfd0ut.tU>tml н.~ @]Conttct.csntml @] lndtushtml • J> b1Юht1'1p llll jquciy Jqu1ty-v.itd1t1on @llJuilyCod•.c.ntmt 0 Applk11ionDbC011tcct ApplicttionDbConltld:.t$ ...... С- 0 M1gг.ttions с• OOOOOCIOOOOOOOO_Crtlt RnttP,щword.uhtml Е'1 RкttPufWOrdConfirm1 · .". 1> J> с- 0 !!) StndCodt.ethtml с- МiмgeControlltr.c1 ..._ &ttlМl.ogWaikимshl: F01gotPaиwo1d.ahtm1 FotgotPщwordConfi @)Lockout.cUltrnl Е:') l891"-clhtml 1!) Rtjiittr.cshtml D flVkon.ko " AttessOenitd.cshtml ~ ~~~~=INtl ::1"'"' о WWWfoot J.ogit1Pwt181.nhtml _V.u.t.onkripьPartlll.cihtml bor.uhtml ~-~pottS.cU!tml @l.V~ohtml Рис. 3.6. Стандартное содержимое, добавляемое в проект шаблонами Empty и Web Application (Model-View-Controller) Дополнительные файлы , которые шаблон Web Application (Model-View-Controller) - всего лишь заполни­ добавляет в проект, выглядят устрашающе, но многие из них тели или реализации, иллюстрирующие общие функциональные возможности. Одни дополнительные файлы настраивают MVC или конфигурируют ASP.NET Core: другие являются библиотеками клиентской стороны, которые будут включаться в состав НТМL-разметки, генерируемой приложением. Список файлов в настоящий момент может выглядеть огромным. но к концу книги вы будете понимать, что делает каж­ дый ИЗ НИХ. Независимо от того. какой шаблон используется для создания проекта, существу­ ют общие папки и файлы, имеющиеся во всех шаблонах. Одни элементы в проекте играют специальные роли, которые жестко закодированы в инфраструктуре Core или МVС либо каком-то инструменте из числа поддерживаемых ASP.NET Visual Studio. Другие связаны с соглашениями об именовании, которые используются в большинс ­ тве проектов ASP.NET Core или MVC. В табл. 3.1 описаны важные файлы и папки, с ASP.NET Core MVC; часть из них по умолчанию не которыми вы столкнетесь в проекте присутствует в проекте, но будет представлена в последующих главах . 83 Глава 3. Паттерн, проекты и соглашения MVC Таблица 3.1. Сводка по элементам проекта Папка или файл Описание /Areas Области - MVC это способ разбиения крупных приложений на мелкие порции. Области рассматриваются в главе /Dependencies 16 Зависимости предоставляют подробные сведения обо всех пакетах, от которых зависит проект. Диспетчеры па­ кетов, применяемые /Components Visual Studio, описаны в главе 6 Здесь определены классы компонентов представлений, которые используются для отображения самодостаточ­ ных средств, таких как корзины для покупок. Компоненты представлений обсуждаются в главе /Controllers 22 Сюда помещаются классы контроллеров. Это соглаше­ ние. Классы контроллеров могут размещаться где угодно, потому что все они компилируются в одну и ту же сборку. Контроллеры подробно рассматриваются в главе /Data 17 Здесь определены классы контекста баз данных, хотя я предпочитаю игнорировать это соглашение и определять их в папке /Data/Migrations Models, как будет продемонстрировано в главе Здесь хранятся миграции Eпtity Framework 8 Саге, чтобы базы данных можно было подготовить для хранения дан­ ных приложения. Миграции будут применяться в главах 8-11 /Models как часть проекта SportsStore Сюда помещаются классы моделей представлений и моделей предметной области. Это соглашение. Классы моделей могут определяться в любом месте текущего проекта или даже в отдельном проекте /Views Здесь хранятся представления и частичные представле­ ния, обычно сгруппированные в папки с именами конт­ роллеров, к которым они относятся. Представления будут подробно описаны в главе /Views/Shared 21 Здесь хранятся компоновки и представления, которые не являются специфичными для каких-то контроллеров. Представления будут подробно описаны в главе /Views/ _ Viewimports. cshtml 21 Этот файл используется для указания пространств имен, которые будут включены в файлы представлений объясняется в главе 5. Razor, ки вспомогательных функций дескрипторов (глава /Views/ ViewStart. cshtml 23) Этот файл позволяет указать стандартную компоновку для механизма визуализации /appsettings. j son как Он также применяется для настрой­ Razor, как описано в главе 5 Этот файл содержит настройки конфигурации, которые можно подгонять под разные среды, такие как среда раз­ работки, среда тестирования и производственная среда. Чаще всего он используется для определения строк под­ ключения к серверу баз данных, а также настроек ведения журналов и отладки (глава 14) 84 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Окончание табл. 3. 1 Папка или файл Описание /bower.json Этот файл содержит список пакетов, управляемых дис­ петчером пакетов /<проект>.jsоn (глава Bower 6) Этот файл содержит конфигурацию проекта, включая пакеты NuGet, которые требуются для приложения, как описано в главах 6 и Данный файл скрыт и может 14. быть отредактирован только путем щелчка правой кноп­ кой мыши на имени проекта в окне выбора в контекстном меню пункта (Редактировать <проект>. /Program.cs Solution Exploreг и Edit <npoeкт>.csproj csproj) Этот класс конфигурирует платформу, на которой разме­ щено приложение (глава 14) /Startup.cs Этот класс конфигурирует само приложение (глава /wwwroot Сюда помещается статическое содержимое, такое как файлы CSS и изображений. Кроме того, именно сюда диспетчер пакетов и Соглашения в проекте CSS 14) (глава Bower устанавливает пакеты JavaScript 6) MVC Есть два вида соглашений в проекте MVC. Первый вид - просто предположения о том. как проект может быть структурирован. Например, пакеты JavaScript и CSS от независимых поставщиков, на которые вы полагаетесь, общепринято помещать в папку wwwroot/ lib. Именно там ожидают их обнаружить другие разработчики MVC, и сюда их будет устанавливать диспетчер пакетов. Но вы вольны переименовать пап­ ку lib или вообще удалить ее и хранить пакеты в другом месте. Это не помешает ин­ фраструктуре link MVC выполнить ваше приложение при условии, что элементы scr ipt и в представлениях ссылаются на местоположение, куда вы поместили пакеты. Второй вид соглашений вытекает из принципа соглашения по конфигурации (или соглашения. над конфигурацией, если делать акцент на преимуществе соглашения пе­ ред конфигурацией), который был одним из главных аспектов, обеспечивших попу­ лярность платформе Ruby оп Rails. Соглашение по конфигурации означает, что вы не должны явно конфигурировать, скажем, ассоциации между контроллерами и их представлениями. Вы просто следуете определенному соглашению об именовании для своих файлов - и все работает. Но когда приходится иметь дело с соглашением такого вида, снижается гибкость в отношении изменения структуры проекта. В последующих разделах объясняются соглашения, которые используются вместо конфигурации. Совет. Все соглашения могут быть изменены путем замены стандартных компонентов MVC собственными реализациями. В книге будут описаны различные способы изменения со­ глашений, что поможет объяснить, как работают приложения MVC, но с рассматриваемы­ ми здесь соглашениями придется иметь дело в большинстве проектов. Глава 3. Паттерн, nроекты и соглашения MVC 85 Следование соглашениям для классов контроллеров Controller, HomeController. При ссылке Классы контроллеров имеют имена, которые заканчиваются на слово например, ProductController, AdminController и на контроллер в другом месте проекта, скажем, в случае применения вспомогательно­ го метода HTML, указывается первая часть имени (вроде МVС автоматически добавляет к такому имени слово Product), Controller а инфраструктура и начинает поиск класса контроллера. Совет. Описанное поведение можно изменить, создав соглашение для моделей, как будет объясняться в главе 31. Следование соглашениям для представлений Представления помещаются в папку по имени Например, представление, ассоциированное с классом в папку /Viеws/ИмяКонтроллера. ProductController, попадет /Views/Product. Controller имени класса в имени папки внут­ Views не указывается, т.е. используется имя /Views/Product, а не /Views/ ProductController. Поначалу такой подход может показаться нелогичным, но он Совет. Обратите внимание, что часть ри быстро войдет в привычку. Инфраструктура MVC ожидает, что стандартное представление для метода дейс­ твия должно иметь имя этого метода. Например, представление, ассоциированное с List. cshtml. Таким обра­ List () класса ProductController стан­ дартным представлением будет /Views/Product/List. cshtml. Стандартное пред­ ставление применяется при возвращении результата вызова метода View () в методе методом действия по имени List (),должно называться зом, ожидается, что для метода действия действия, примерно так: return View(); Можно указать имя другого представления: return View("MyOtherView"}; Обратите внимание. что мы не включаем в представление расширение имени MVC просматривает /Views/Shared. Это значит, что файла или путь. При поиске представления инфраструктура папку, имеющую имя контроллера, а затем папку представления, которые будут использоваться более чем одним контроллером, можно поместить в папку /Views/Shared и MVC успешно найдет их. Следование соглашениям для компоновок Соглашение об именовании для компоновок предусматривает снабжение имени файла префиксом в виде символа подчеркивания ки внутри папки /Views/Shared. (_) и размещение файлов компонов­ Стандартная компоновка применяется по умолча­ нию ко всем представлениям через файл /Views/ _ ViewStart. cshtml. Если вы не хотите, чтобы стандартная компоновка применялась к представлениям, тогда можете Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 86 изменить настройки в файле_ViewStart. cshtml (либо вообще удалить его), указав другую компоновку в представлении, например: @{ Layout = "-/_MyLayout.cshtml"; Или же можете отключить компоновку дпя заданного представления: @{ Layout = null; Резюме В главе был представлен архитектурный паттерн MVC и его сравнение с несколь­ кими другими паттернами, с которыми вы могли сталкиваться или слышать о них ра­ нее. Вы узнали о важности модели предметной области и ознакомились с концепцией разделения обязанностей, которая позволяет отвязывать компоненты друг от друга дпя обеспечения строгого отделения частей приложения. В следующей главе объясня­ ется структура проектов MVC в Visual Studio и рассматриваются важнейшие средства языка С#, которые используются при разработке веб-приложений MVC. ГЛАВА 4 Важные функциональные возможности языка С# в этой главе будут описаны функциональные возможности С#, используемые при разработке веб-приложений, которые не очень хорошо понимают или часто пу­ тают. Однако настоящая книга не посвящена языку С#, так что для каждой возмож­ ности будет приводиться только краткий пример, поэтому вы можете проработать примеры из остальных глав книги и задействовать нужные возможности в своих про­ ектах. В табл. Таблица 4. 1. 4.1 приведена сводка для данной главы. Сводка по главе Листинг Решение Задача Избегание обращения к свойствам Используйте по ссылкам операцию null Упрощение свойств С# 4.5-4.8 nul 1-условную Используйте автоматически реа­ 4.9-4.11 лизуемые свойства Используйте интерполяцию строк 4.12 Создание объекта и установка его свойств Используйте инициализатор объ- 4.13-4.16 за один шаг екта или коллекции Упрощение формирования строк Проверка типа или характеристик объекта 4.17-4.18 Используйте сопоставление с образцом Добавление функциональности в класс, который не может быть модифицирован Используйте расширяющий метод 4.19-4.26 Упрощение использования делегатов Используйте лямбда-выражение 4.27-4.34 Применение неявной типизации Используйте ключевое слово 4.35 Создание объектов без определения типа Используйте анонимный тип 4.36, 4.37 Упрощение использования асинхронных Используйте ключевые слова 4.38-4.41 методов async Получение имени метода или свойства Используйте выражение и однострочных методов класса без определения статической строки и var await nameof 4.42, 4.43 88 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Подготовка проекта для примера для Создадим Visual Studio в главы текущей новый по проект имени LanguageFeatures, используя шаблон ASP.NET Саге Web Application (Веб-приложение ASP.NEТ Core) из группы .NET Core (рис. 4.1 ). Когда отобразятся различные конфигурации проекта, выберем шаблон (Пустой). как показано на рис. 4.2. Выберем варианты списках в верхней части окна и удостоверимся в том, что No Authentication (Аутентификация) установлен в флажок ЕпаЫе Docker Support Empty ASP.NET Core 2.0 в переключатель Authentication .NET Core и (Аутентификация отсутствует). а (Включить поддержку Docker) не отмечен. Щелкнем на кнопке ОК. чтобы создать проект. №.YOPIVJ~t 1 1 ' • .NET Frмi~k".6.1 Р· ~ ConscНApp (.NHCOftJ "' V1su•I<• о.;~ :i;il! \l/indows с~.ик Otsktop Vleb fj \iJ .Nff Ccкe; .NПSt•ndмd Cloud Tts! t Х ' _3 SortЬy. !Dtft\i Туре: Visual' С• Ptoject tO"l'lplltnfor CJUt.ing ASP.NET Co1t 'Pphntющ for Windows, LinUllt •nd m•(OS Vllfl.9 .NЕТ (ore or .Ж Т Ою LtЬr•ry (.NЕТ CortJ ~r•mewod'. Uм ltst P•ojt<< f,NH Со") .u..т"'"'°"''(.NЕТСо") Visu•l~sic j SQLS.М< Not f1ndin9wh.t )'OU vt. loolang fof? Opcn V~i \WdIO · миuс1 '~" - -~-~~~_,..., !9_u;;""'""~ Ui\guagitfиitvfu ~-~-~~-~~ ...__~ о.:,м;.-=-Рис. . .NfТCort 1 Rttct.js Е'О] Y.i~API ASP.N<f C0<t 2.0 iП Wtb Applkation 4.1. • 0 в.-._ Crtat~dlrtdoryfor,~ О AddtoSour,e.(ontroJ J .~~~~ ~~OK~~~C..ncd Выбор типа проекта 1.u!!!-"-'Ш аю о \'/tb Angul•r An ltmf)t)'prt;ed ~emp&.t• f0tc1e•tJng tn ASP.NEТ Cott •pplkat..on. Thts templtte dca not hwt .ny conttntinit. Applicltюn {Мodtl·V1t..,... Controtlв) &Ь Ruct.Jsand ........ Authentkмion No AuthentЬtion 0 En•Ьl•Dod...~ СУ..: >\t"rndoW', Rtqu11ts [)qd;cr for ifmdgW$ Oocktr suppcrt ctn Юо Ье eivbled i.te' ~ COCJ~ - - - -· --· --·----- L------·- Рис. 4.2. Выбор шаблона проекта Глава 4. Важные функциональные возможности языка С# Включение 89 ASP.NET Core MVC Empty создает проект, который содержит минимальную конфи­ ASP.NET Core без какой-либо поддержки MVC. Это значит, что содержи­ мое-заполнитель, добавляемое шаблоном Web Application (Model-View-Controller) (Веб­ Шаблон проекта гурацию приложение (модель-представление-контроллер)), отсутствует, но также означает необходимость в выполнении ряда дополнительных шагов для включения МVС, чтобы заработали такие средства, как контроллеры и представления. В данном разделе мы внесем изменения, требуемые для включения MVC в проекте, но пока не будем вда­ ваться в детали каждого шага. Чтобы включить инфраструктуру веденные в листинге Листинг using using using using using using using using 4.1. MVC, внесем в класс Startup изменения, при­ 4.1. Включение MVC в файле Startup. cs из папки LanguageFea tures System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; namespace LanguageFeatures puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); puЫic void Configure(IApplicationBuilder if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); арр, IHostingEnvironment env) { ) 11 app.Run(async (context) => { 11 awai t context. Response. Wri teAsync ( "Hello World ! ") ; 11 } ) ; app.UseМvcWithDefaultRoute(); Конфигурирование приложений ASP.NEТ Core МVС будет объясняться в главе пока достаточно знать, что два оператора, добавленные в листинге базовую настройку MVC 4.1, 14, а обеспечивают с применением стандартной конфигурации и соглашений. Создание компонентов приложения MVC Имея настроенную инфраструктуру МVС, можно заняться добавлением компонен­ тов приложения MVC, ковых средств С#. которые будут использоваться для демонстрации важных язы­ Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 90 Создание модели Начнем с создания простого класса модели, чтобы иметь какие-то данные, с кото­ рыми можно работать. Добавим папку по имени Models и создадим в ней файл класса 4.2. Product. cs с определением, показанным в листинге Листинг Содержимое файла 4.2. Product. cs из папки Models namespace LanguageFeatures.Models { puЬlic class Product { puЫic string Name ( get; set; } puЫic decimal? Price ( get; set; puЫic static Product[] GetProducts() Product kayak = new Product ( Name = "Kayak", Price = 275М }; Product lifejacket = new Product Name = "Lifejacket", Price = 48.95М ) ; return new Product[] ( kayak, lifejacket, null ); Products определены свойства Name и Price, а также статический ме­ GetProducts (),который возвращает массив элементов Product. Один из элементов, содержащихся в возвращаемом из метода GetProducts () массиве, установлен в null; он будет применяться для демонстрации ряда языковых средств В классе тод по имени позже в главе. Создание контроллера и представления В примерах настоящей главы для демонстрации различных языковых средств мы используем простой контроллер. Создадим папку класса по имени ге 4.3. HomeController. cs, Controllers и добавим в нее файл содержимое которого приведено в листин­ В случае применения стандартной конфигурации инфраструктура по умолчанию отправлять НТГР-запросы контроллеру Листинг 4.3. Содержимое файла HomeController. cs MVC будет Home. из папки Controllers using Microsoft.AspNetCore.Mvc; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЬlic ViewResult Index() return View(new string[] ( "С#", "Language", "Features" )) ; Метод действия Index () сообщает инфраструктуре MVC о необходимости визу­ ализировать стандартное представление и передает ей массив строк, который дол- Глава 4. Важные функциональные возможности языка С# 91 жен быть включен в НТМL- разметку . отправляемую клиенту. Чтобы создать соответс­ твующее представление, добавим папку а затем внутри нее папку Index. cshtml Home) Views/Home (сначала создав папку Views, и поместим в нее файл представления по имени с содержимым, показанным в листинге 4.4. Листинг 4.4. Содержимое файла Index. cshtml из папки Views/Home @model IEn umeraЫe<string> @{ Layout = null; < ! DOCTYPE html> <html> <head> <meta name="viewport" co r1t e n t= " wi dth = device-w i dth" /> <title> Language Features</tit le > </head> <body> <ul> @foreach (string s in Model ) { <li>@s</li> </ul> </bod y > </html> В результате запуска примера приложения за счет выбора пункта (Запустить отладку) в меню Debug 1-- Start Debugging (Отладка) по.явится вьmод, представленный на рис. -- 4.3. - С LФ~cal~ost:б2~5 • С:;: • Language • Features Рис. 4.3 . Запуск пример приложения Поскольку выводом во всех примерах данной главы является текст. отображаемые браузером сообщения в дальнейшем будут показаны так: С# Language features Использование null-условной операции С помощью null-условной операции можно более элегантно обнаруживать зна­ чения nul l. Во время разработки приложений МVС встречается много проверок на 92 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 предмет null, т.к. нужно выяснять, содержит ли запрос специфический заголовок или значение либо содержит ли модель определенный элемент данных. Традиционно работа со значениями null требовала явных проверок, которые могли становиться утомительными и подверженными ошибкам, когда требовалось инспектировать и объект, и его свойства. За счет применения null-условной операции такие проверки будут намного легче и компактнее (листинг Листинг 4.5. Обнаружение значений из папки null 4.5). в файле HomeController. сз Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЬlic class HomeController : Controller puЫic ViewResult Index() ( List<string> results = new List<string>(); foreach (Product р in Product.GetProducts()) string name = р? .Name; decimal? price = р? . Price; resul ts .Add (string. Format ( "Name: {О) , Price: {1)", name, price)) ; return View(results); Статический метод GetProducts (), определенный в классе Product, возвра­ Index () конт­ щает массив объектов, который инспектируется в методе действия роллера с целью получения списка значений Narne и Price. Проблема в том, что и null, т.е. нельзя просто ссы­ foreach, не получив исключение объект в массиве, и значения его свойств могут быть Narne или р. Price внутри цикла NullReferenceException. Во избежание такой латься нар. ситуации используется null-услов­ ная операция: string name = p?.Name; decimal? price = p?.Price; null-условная операция обозначается знаком вопроса(?). Если значение р равно null, то переменная narne также но null, тогда переменной narne будет установлена в null. Если значение р не рав­ будет присвоено значение свойства Аналогичной проверке подвергается и свойство Price. Person. Narne. Обратите внимание, что пе­ ременная, которой выполняется присваивание с применением null-условной опера­ ции, должна быть в состоянии иметь дело со значениями price объявлена с десятичным типом, допускающим null, поэтому null (decirnal ?). переменная Связывание в цепочки null-условных операций Для навигации по иерархии объектов null-условные операции могут связываться в цепочки и превращаться в по-настоящему эффективный инструмент для упрощения Глава 4. Важные функциональные возможности языка С# кода и обеспечения безопасной навигации. В листинге 4.6 к классу Product 93 добавлено свойство с вложенными ссылками, что создает более сложную иерархию объектов. Листинг 4.6. Добавление свойства в файле Product.cs из папки Мodels namespace LanguageFeatures.Models { puЫic class Product { puЬlic string Name { get; set; 1 decimal? Price { get; set; ) puЬlic Product Related { qet; set; } puЬlic static Product[] GetProducts() Product kayak = new Product 1 Name = "Kayak", Price = 275М puЫic ); Product lifejacket = new Product Name = "Lifejacket", Price = 48.95М ); kayak.Related = lifejacket; return new Product[J 1 kayak, lifejacket, null 1; Product имеет свойство Related, которое может ссылаться Product. В методе GetProducts {) мы устанавливаем свойство объекта Product, представляющего каяк. В листинге 4.7 показано, как Каждый объект на другой объект Related для можно соединять вместе null-условные операции для навигации по свойствам объ­ ектов, не вызывая исключение. Листинг 4.7. Обнаружение вложенных значений из папки null в файле HomeController.cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЬlic class HomeController : Controller ViewResult Index() { List<string> results = new List<string>(); foreach (Product р in Product.GetProducts()) string name = p?.Name; puЫic decimal? price = p?.Price; strinq related.Name = p?.Related?.Name; results.Add(strinq.Format("Name: (0}, Price: (1}, Related: {2}", name, price, related.Name)); return View(results); 94 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 null-условную операцию можно применять к каждой части цепочки свойств, например: string relatedName = p?.Related?.Name; relatedName получит значение null. когда значение Related. Иначе relatedName будет присвоено значение свойс­ р. Rela ted. Name. После запуска примера приложения в окне браузера появится В таком случае переменная null тва имеет р или р. следующий вывод: Name: Kayak, Price: 275, Related: Lifejacket Name: Lifejacket, Price: 48.95, Related: Name: , Price: , Related: Комбинирование null-условной операции и операции объединения с null Для установки альтернативного значения. представляющего null. удобно комби­ нировать null-условную операцию (один знак вопроса) и операцию объединения с null (два знака вопроса), как демонстрируется в листинге Листинг 4.8. Сочетание операций работы с из папки 4.8. null в файле HomeCon troller. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using Languagefeatures.Models; namespace Languagefeatures.Controllers puЫic class HomeController : Controller ViewResult Index() { List<string> results = new List<string>(); foreach (Product р in Product.GetProducts()) string name = р?. Name ?? "<No Name>"; decimal? price = р?. Price ?? О; string relatedName = р? .Related? .Name ?? "<None>"; results.Add(string.Format("Name: {0}, Price: {1}, Related: {2}", name, price, relatedName)); puЫic return View(results); null-условная операция гарантирует, что при навигации по свойствам объектов не возникнет исключение null NullReferenceException, а операция объединения с null в результатах, отображаемых в брау­ обеспечивает отсутствие значений зере. В результате запуска примера приложения в окне браузера будет получен такой вывод: Name: Kayak, Price: 275, Related: Lifejacket Name: Lifejacket, Price: 48.95, Related: <None> Name: <No Name>, Price: О, Related: <None> Глава 4. Важные функциональные возможности языка С# 95 Использование автоматически реализуемых свойств В языке С# поддерживаются автоматически реализуемые свойства, которые мы применяли при определении свойств класса Person в предыдущем разделе: namespace LanguageFeatures.Models { puЫic class Product { puЬlic { get; set; } Price { get; set; Product Related { get; set; } puЫic static Product[] GetProducts() puЫic string Nаш.е puЬlic deciшal? 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 }; Данное средство дает возможность определять свойства, не реализуя блоки кода get и set. Средство автоматически реализуемых свойств позволяет трактовать сле­ дующее определение свойства: puЫic string Name { get; set; } как эквивалентное приведенному ниже коду: string Name { get { return name; set { name = value; } puЫic Средства подобного типа известны как "синтаксический сахар", который делает работу с С# более приятной (в этом случае за счет устранения избыточного кода. дуб­ лируемого в итоге для каждого свойства), однако существенно не изменяет поведение языка. Термин "сахар" может показаться умаляющим ценность средства, но любые улучшения, которые облегчают написание и сопровождение кода, приносят пользу - особенно в крупных и сложных проектах. Использование инициализаторов автоматически реализуемых свойств Автоматически реализуемые свойства поддерживаются, начиная с версии С# 3.0. В последней версии языка С# доступны инициализаторы для автоматически реали- 96 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 зуемых свойств. которые позволяют устанавливать начальные значения, не требуя применения конструкторов (листинг Листинг 4.9. 4.9). Использование инициализатора автоматически реализуемого свойства в файле Product. cs из папки Мodels namespace LanguageFeatures.Models { puЬlic class Product { puЫic puЬlic puЫic puЫic string Name { get; set; } string Category { get; set; } = "Watersports"; decimal? Price { get; set; } Product Related { get; set; } puЫic static Product[] GetProducts() Product kayak = new Product { 11 Name = кауаk", Category = "Water Craft", Price = 275М }; Product lifejacket = new Product { Name = "Lifejacket", Price = 48.95М }; kayak.Related = lifejacket; return new Product[] { kayak, lifejacket, null }; Присваивание значения автоматически реализуемому свойству не препятствует применению установщика для изменения свойства в более позднее время. а всего лишь приводит в порядок код для простых типов с конструкторами, которые содер­ жат список присваиваний свойств, обеспечивающих наличие стандартных значений. В приведенном примере инициализатор присваивает свойству Category значение "Wa tersports". Начальное значение может быть изменено, что и делается при со­ здании объекта kayak, когда ему указывается значение "Wa ter Craft". Создание автоматически реализуемых свойств только для чтения Для создания свойства только для чтения в определении автоматически реализу­ емого свойства. которое имеет инициализатор, нужно опустить ключевое слово (листинг Листинг 4.1 О). 4.1 О. Создание свойства только для чтения в файле Product. cs из папки Мodels namespace LanguageFeatures.Models puЫic class Product { puЫic puЫic puЫic puЫic puЬlic string Name { get; set; ) string Category { get; set; decimal? Price { get; set; ) Product Related { get; set; ) bool InStock { get; } = true; "Watersports"; set Глава 4. Важные функциональные возможности языка С# puЬlic 97 static Product[) GetProducts() Product kayak = new Product Narne = "Kayak", Category = "Water Craft", Price = 275М ); Product lifejacket = new Product { Narne = "Lifejacket", Price = 48.95М }; kayak.Related = lifejacket; return new Product[J { kayak, lifejacket, null }; Свойство InStock инициализируется значением true и не может быть изменено; тем не менее, ему можно присвоить значение внутри конструктора типа, как показа­ но в листинге Листинг 4. 11. 4.11. Присваивание значения свойству только для чтения в файле Product. cs из папки Models narnespace LanguageFeatures.Models { puЫic class Product { Product (bool stock InStock = stock; puЬlic puЫic puЫic puЫic puЫic puЬlic = true) string Narne { get; set; } string Category { get; set; decirnal? Price { get; set; } Product Related { get; set; } bool InStock { get; } "Watersports"; puЫic static Product[] GetProducts() Product kayak = new Product { Narne = "Kayak", Category = "Water Craft", Price = 275М }; Product lifejacket = new Product (false) Name = "Lifejacket", Price = 48. 95М } ; kayak.Related = lifejacket; return new Product[J { kayak, lifejacket, null }; Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 98 Конструктор позволяет указывать в качестве аргумента значение для свойства, допускающего только чтение, и если значение не предоставлено, тогда он устанавли­ вает свойство в стандартное значение true. После установки конструктором значе­ ния свойства оно не может быть изменено. Использование интерполяции строк Традиционным инструментом С# для образования строк, содержащих значения данных, является метод контроллере string. Format ().Вот пример применения этого приема в Home: results.Add(string.Format( "Name: {О}, Price: {1}, Related: {2}", name, price, relatedName)); Язык С# также поддерживает другой подход, называемый интерполяцией строк, который позволяет избежать необходимости гарантировать, что ссылки вида и ( 1) ( О ), в шаблоне строки соответствуют переменным, указанным в качестве ар­ {2) гументов. Взамен интерполяция строк использует имена переменных напрямую (лис­ тинг 4.12). Листинг 4.12. Применение интерполяции строк в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc ; using System.Collections.Gener ic; using LanguageFeatures.Models; namespace LanguageFeatures.Control lers puЫic class HomeController : Controller puЬlic 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($ 11 Name: {name}, Price: {price}, Related: {relatedName}"); return View(results); Интерполированная строка снабжается префиксом в виде символа $ и содержит "дыры", которые представляют собой ссылки на значения, содержащиеся внутри фи­ гурных скобок ( ( и ) ). Когда строка оценивается, "дыры" заполняются текущими зна­ чениями указанных переменных и констант. Среда Visual Studio обеспечивает поддержку средства IntelliSense для создания интерполированных строк и предлагает список доступных членов, когда набирается символ открывающей фигурной скобки, что помогает минимизировать количество опечаток, а результатом оказывается формат строки, который легче понять. Глава 4. Важные функциональные возможности языка С# 99 Совет. Интерполяция строк поддерживает все спецификаторы формата, которые доступ­ ны для метода string. Forma t (). Спецификаторы формата включаются в виде части Price: {price: С2}" сформатирует значение price как денежное "дыры", поэтому$" значение с двумя десятичными цифрами. Использование инициализаторов объектов и коллекций При создании объекта в статическом методе GetProducts () класса Product при­ менялся инициализатор объекта, который позволяет создавать объект и указывать значения его свойств за один шаг, например: Product kayak = new Product "I<ayak" , Name Cateqory = "Water Craft", Price = 275М = ) ; Это еще одна форма "синтаксического сахара", делающая язык С# легче в исполь­ зовании. Без такого средства пришлось бы вызывать конструктор Product и затем применять вновь созданный объект для установки всех его свойств: Product kayak = new Product(); kayak.Name = "Kayak"; kayak.Category = "Water Craft"; kayak.Price = 275М; Связанное с ним средство - инициализатор коллекции - позволяет создавать коллекцию и указывать ее содержимое за один шаг. Без такого инициализатора со­ здание, скажем, массива потребовало бы указания размера и элементов массива по отдельности (листинг Листинг 4.1 З. 4.13). Инициализация массива в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЬlic ViewResult Index() { strinq[] names = new strinq[З]; "ВоЬ"; names [О] "Joe"; names[l] names [2] = "Alice"; return View("Index", names); = 100 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Инициализатор коллекции дает возможность указать содержимое массива как часть его конструирования, что неявно предоставляет компилятору размер массива 4.14). (листинг Листинг 4.14. Использование инициализатора коллекции в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResul t Index () { return View("Index", new string[] ( puЫic "ВоЬ", Элементы массива задаются между символами "Joe", "Alice" }) ; { и f • что позволяет получить более краткое определение коллекции и делает возможным определять коллекцию внутри вызова метода. Код в листинге 4.14 дает тот же результат, что и код в листинге 4.13; если запустить пример приложения, то в окне браузера получится следующий вывод: ВоЬ Joe Alice Использование инициализатора индексированной коллекции В последних версиях С# улучшен способ инициализации коллекций с индексами, таких как словари. В листинге 4.15 приведен код метода действия Index (), перепи­ санный для определения коллекции с помощью традиционного подхода С# к иници­ ализации словаря. Листинг 4.15. Инициализация словаря в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResult Index() { Dictionary<string, Product> products = new Dictionary<string, Product> ( ( "Кауаk", new Product ( Nаше = "Кауаk", Price = 275М } } , ( "Lif'ejacket", new Product( Name = "Lif'ejacket", Price = 48. 95М } } puЫic }; return View("Index", products .Keys); Глава 4. Важные функциональные возможности языка С# 101 Синтаксис для инициализации коллекции такого типа слишком сильно полагает­ ся на символы { и }, особенно когда значения коллекции создаются с применением инициализаторов объектов. В последних версиях С# поддерживается более естест­ венный подход к инициализации индексированной коллекции, который согласован со способом извлечения или модификации значений после того, как коллекция была инициализирована (листинг Листинг 4. 16. 4.16). Использование синтаксиса инициализатора коллекции в файле HomeController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResult Index() { Dictionary<string, Product> products = new Dictionary<string, Product> { puЬlic ["Кауаk"] = new Product { Name = "Кауаk", Price = 275М }, [ "Lif'ejacket"] = new Product { Name = "Lifejacket", Price = 48. 95М ); return View("Index", products.Keys); Результат остается прежним. т.е. создание словаря, ключами которого являются Kayak и Lifejacket, а значениями - объекты Product, но элементы создаются с применением системы обозначений для индексов, используемой в других операци­ ях с коллекциями. Вот вывод. получаемый в окне браузера после запуска примера приложения: Kayak Lifejacket Сопоставление с образцом Одним из наиболее полезных недавних добавлений в язык С# стала поддержка со­ поставления с образцом, которую можно использовать для проверки, что объект отно­ сится к определенному типу или имеет специфические характеристики. Сопоставление с образцом - еще одна форма "синтаксического сахара", и она способна значительно упростить сложные блоки условных операторов. Ключевое слово выполнения проверки типа. как демонстрируется в листинге Листинг 4. 17. Выполнение проверки типа в файле из папки Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller i s применяется для 4.17. HomeCon trol ler. cs 102 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 puЬlic ViewResult Index() { object [] data = new object [] { 275М, 29. "apple", "orange", 100, 10 } ; decimal total = О ; for (int i = О; i < data.Length; i++) { if (data[i] is decimal d) { total += d; 95М, return View("Index", new string[] { $"Total: {total:C2}" }) ; Ключевое слово is выполняет проверку типа, и если значение принадлежит ука­ занному типу, тогда оно присваивается новой переменной: if (data [i] is decimal d) { Выражение будет оценено как тип decimal. Значение data (i] true, если значение, хранящееся в будет присваиваться переменной data [ i], имеет d, что позволит его использовать в последующих операторах без необходимости в каких-либо пре­ образованиях типов. Ключевое слово is даст совпадение лишь с указанным типом. т.е. будут обработаны только два значения в массиве массиве относятся к типам string и int). data [] (остальные элементы в В результате запуска приложения в окне браузера появится следующий вывод: Total: $304.95 Сопоставление с образцом в операторах swi tch Сопоставление с образцом может также применяться в операторах рые поддерживают ключевое слово сопоставляется в операторе Листинг case when (листинг 4.18). 4.18. Сопоставление с образцом в файле HomeController.cs из папки swi tch, кото­ для ограничения случаев, когда значение Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResult Index() { object[] data = new object[] { 275М, "apple", "orange", 100, 10 1; decimal total = О; puЬlic 29.95М, for (int i =О; i < data.Length; i++} { switch (data[i]) { case deciшal deciшalValue: total += deciшalValue; break; 103 Глава 4. Важные функциональные возможности языка С# case int intValue when intValue > 50: total += intValue; break; return View("Index", new string[] { $"Total: {total:C2}" }) ; Для сопоставления любого значения специфического типа в операторе case ис­ пользуются тип и имя переменной, например: case decimal decimalValue: Такой оператор case сопоставляется с любым значением его новой переменной по имени ности, можно добавить decimalValue. ключевое слово when: decirnal и присваивает Чтобы достичь большей избиратель­ case int intValue when intValue > 50: case сопоставляет значения типа int и присваивает их перемен­ intValue, но лишь когда значение больше 50. Запуск приложения при­ Этот оператор ной по имени водит к следующему выводУ в окне браузера: Total: $404.95 Использование расширяющих методов Расширяющие методы - удобный способ добавления методов в классы, владель­ цем которых вы не являетесь и не можете модифицировать напрямую. В листин­ ге 4.19 приведено определение класса ShoppingCart, добавленного в папку Models в ShoppingCart. cs, который представляет коллекцию объектов Product. виде файла Листинг 4.19. Содержимое файла ShoppingCart. cs из папки Models using System.Collections.Generic; namespace LanguageFeatures.Models puЫic class ShoppingCart { puЬlic IEnumeraЫe<Product> Products { get; set; } Это простой класс, который действует как оболочка для последовательности объек­ тов Product (в данном примере необходим лишь элементарный класс). Предположим, что нас интересует возможность определения общей стоимости объектов классе ShoppingCart, Product в но мы не можем изменить сам класс из-за того, что он пос­ тупил от третьей стороны и его исходный код отсутствует. Для добавления нужной функциональности можно применить расширяющий метод. В листинге зан класс MyExtensionMethods, MyExtensionMethods.cs. также добавленный в папку Models 4.20 пока­ в виде файла 104 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Листинг 4.20. Содержимое файла МyExtensionМethods. cs из папки Мodels namespace LanguageFeatures.Models { puЫic static class MyExtensionMethods static decimal TotalPrices(this ShoppingCart cartParam) { decimal total = О; foreach (Product prod in cartParam.Products) total += prod?.Price ?? О; puЫic return total; Ключевое слово TotalPrices () this, расположенное перед первым параметром, помечает как расширяющий метод. Первый параметр указывает .NЕТ, к ка­ кому классу может применяться расширяющий метод случае. На экземпляр класса ShoppingCart, - к ShoppingCart в данном к которому применен расширяющий ме­ cartParam. Расширяющий метод Product в ShoppingCart и возвращает сумму значений их свойств Product. Price. В листинге 4.21 демонстрируется применение расширяю­ щего метода в методе действия контроллера Home. тод, можно ссьmаться с использованием параметра проходит по объектам На заметку! Расширяющие методы не позволяют нарушать правила доступа, которые клас­ сы определяют для своих методов, полей и свойств. С помощью расширяющего метода функциональность класса можно расширить, но с использованием только тех членов, к которым в любом случае имеется доступ. Листинг 4.21. Применение расширяющего метода в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResult Index{) { ShoppingCart cart = new ShoppingCart { Products = Product. GetProducts () } ; decimal cartтotal = cart.TotalPrices(); return View ( 11 Index 11 , new string [] { $ "Total: {cartTotal: С2} " } ) ; puЬlic Вот ключевой оператор: decimal cartTotal = cart.TotalPrices(); Глава 4. Важные функциональные возможности языка С# Метод 105 TotalPrices () вызывается на объекте ShoppingCart. как если бы он бьm ShoppingCart, хотя он представляет собой расширяющий метод. ко­ частью класса торый определен в совершенно другом классе. Среда .NЕТ будет обнаруживать рас­ ширяющие классы, если они находятся в области действия текущего класса, т.е. яв­ ляются частью того же самого пространства имен или пространства имен, которое указано в операторе using. В результате запуска примера приложения в окне браузе­ ра появится следующий вывод: Total: $323. 95 Применение расширяющих методов к интерфейсу Можно также создавать расширяющие методы, которые применяются к интерфей­ су, что позволит вызывать такие расширяющие методы для всех классов, реализую­ щих данный интерфейс. В листинге 4.22 приведен код класса ShoppingCart, моди­ фицированный с целью реализации интерфейса IEnumeraЫe<Product>. Листинг 4.22. Реализация интерфейса в файле ShoppingCart. cs из папки Мodels using System.Collections; using System.Collections.Generic; namespace LanguageFeatures.Models puЫic class ShoppingCart : IEnumeraЬle<Product> puЫic IEnumeraЬle<Product> Products { get; set; IEnumerator<Product> GetEnumerator () return Products.GetEnumerator(); puЬlic IEnumerator IEnumeraЬle. GetEnumerator () return GetEnumerator(); Теперь расширяющий метод можно изменить так, чтобы он работал с интерфей­ сом IEnumeraЬle<Product> (листинг Листинг 4.23. 4.23). Модификация расширяющего метода в файле МyExtensionМethods. из папки Models using System.Collections.Generic; namespace LanguageFeatures.Models puЫic static class MyExtensionMethods puЬlic static decimal TotalPrices (this decimal total = О; foreach (Product prod in products) total += prod?. Price ?? return total; О; IEnumeraЬle<Product> products) cs 106 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 тип первого параметра бьш изменен на IEnumeraЫe<Product>. а это значит. что цим foreach в теле метода работает непосредственно с объектами Product. Переход на использование упомянутого интерфейса означает, что мы можем подсчитать об­ щую стоимость объектов Product, перечисляемых посредством любого интерфейса IEnumeraЫe<Product>, что вмючает не только экземпляры же массивы объектов Листинг 4.24. Product (листинг ShoppingCart, но так­ 4.24). Применение расширяющего метода к массиву в файле HomeController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЬlic ViewResult Index() { ShoppingCart cart = new ShoppingCart { Products = Product.GetProducts() ); Product [] productArray = { new Product {Name = 11 Kayak 11 , Price = 275М}, new Product {Name = 11 Lifejacket 11 , Price = 48. 95М} }; decimal cartTotal = cart.TotalPrices(); decimal arrayTotal = productArray.TotalPrices(); return View( 11 Index 11 , new string[] $ 11 Cart Total : { cartTotal : С2} 11 , $ 11 Array Total: { arrayTotal: С2} 11 } ) ; После запуска проекта будут получены показанные ниже результаты. которые де­ монстрируют, что расширяющий метод возвращает один и тот же результат незави­ симо от способа перебора объектов Product: Cart Total: $323.95 Array Total: $323. 95 Создание фильтрующих расширяющих методов Последний аспект расширяющих методов, о котором необходимо упомянуть - возможность их использования для фильтрации коллекций объектов. Расширяющий метод, который оперирует на интерфейсе IEnumeraЬle<T> и также возвращает IEnumeraЬle<T>, может задействовать мючевое слово yield, чтобы применить критерий отбора к элементам в источнике данных с целью генерации сокращенного набора результатов. В листинге в класс MyExtensionMethods. 4.25 представлен такой метод. который добавляется Глава 4. Важные функциональные возможности языка С# Листинг 4.25. 107 Добавление фильтрующего расширяющего метода в файле МyExtensionМethods. cs из папки Controllers using System.Collections.Gener ic; namespace LanguageFeatures.Models puЫic static class MyExtensionMethods static decimal TotalPrices(this decimal total = О; foreach (Product prod in products) total += prod?.Price ?? О; puЫic IEnumeraЫe<Product> products) { return total; static IEnumeraЫe<Product> FilterByPrice( this IEnwneraЫe<Product> productEnwn, decimal minimumPrice) foreach (Product prod in productEnum) { if ( (prod?. Price ?? 0) >= minimumPrice) yield return prod; puЫic Расширяющий метод по имени Fi 1 t е r В у Р r i се () принимает дополнительный параметр, который позволяет фильтровать товары. так что в результате возвращают­ ся объекты Product, у которых значение свойства Price совпадает или превышает FilterByPrice () демонс­ значение. указанное в параметре. Использование метода трируется в листинге Листинг 4.26. 4.26. Применение фильтрующего расширяющего метода в файле HomeController. cs из папки Controllers using Microsoft.AspNetCore.Mvc ; using System.Collections.Gener ic; using LanguageFeatures.Models; namespace LanguageFeatures.Control lers puЬlic class HomeController : Controller ic ViewResul t Index () { Product[J 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", Price = 34. 95М} рuЫ }; decimal arrayTotal = productArray.FilterByPric e(20) .TotalPrices(); return View("Index", new string[] { $"Array Total: {arrayТotal:C2}" }) ; 108 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 При вызове метода TotalPrices () FilterByPrice () на массиве объектов Product метод получает и использует для подсчета суммы только те из них, кото­ рые стоят больше $20. В результате запуска примера приложения в окне браузера отобразится следующий вывод: Total: $358.90 Использование лямбда-выражений Лямбда-выражения являются функциональной возможностью, ставшей причиной многочисленных заблуждений - не в последнюю очередь из-за того, что упрощаемое с их помощью средство само вызывает путаницу. Чтобы понять решаемую задачу, рассмотрим расширяющий метод FilterByPrice (),который был определен в пре­ дыдущем разделе. Метод реализован так, что он может фильтровать объекты Product по цене, а это значит, что если понадобится фильтровать объекты по названию, тогда придется создать второй метод вроде приведенного в листинге Листинг 4.27. 4.27. Добавление фильтрующего метода в файле МyExtensionмethods. cs из папки Models using System.Collections.Generic; namespace LanguageFeatures.Models puЫic static class MyExtensionMethods puЫic static decimal TotalPrices(this decimal total = О; foreach (Product prod in products) total += prod?.Price ?? О; IEnumeraЬle<Product> products) return total; puЫic static IEnumeraЫe<Product> FilterByPrice( this IEnumeraЫe<Product> productEnum, decimal minimumPrice) { foreach ( Product prod in productEnum) { if ( (prod?.Price ?? 0) >= minimumPrice) yield return prod; static IEnwneraЬle<Product> FilterByName ( this IEnwneraЬle<Product> productEnum, char firstLetter) foreach (Product prod in productEnum) { if (prod?.Name?[O] == firstLetter) { yield return prod; puЬlic В листинге 4.28 демонстрируется применение обоих фильтрующих методов внут­ ри контроллера для создания двух разных итоговых сумм. Глава 4. Важные функциональные возможности языка С# Листинr 109 4.28. Использование двух фильтрующих методов в файле HomeController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЫic ViewResult Index() { Product [] productArray = { "Kayak", Price = 275М}, new Product {Name "Lifej acket", Price = 4 8. 95М}, new Product { Name 19.SOM}, "Soccer ball", Price new Product {Name "Corner flag", Price = 34.95М} new Product {Name }; decimal priceFilterTotal =productArray.FilterВyPrice(20) .TotalPrices(); decimal nameFil terTotal = productArray. Fil terВyName ( ' S' ) . TotalPrices () ; return View (" Index" , new string [] { $ "Price Total: {priceFil terTotal: С2)" , $ "Name Total: {nameFil terTotal: С2}" } ) ; Первый фильтр отбирает все товары с ценой ры с названиями, начинающимися на букву S. $20 и выше, а второй фильтр - това­ После запуска примера приложения в окне браузера появится такой вывод: Price Total: $358.90 Name Total: $19.50 Определение функций Описанный выше процесс можно повторять до бесконечности и создавать филь­ трующие методы для каждого интересующего свойства и сочетаний свойств. Более элегантный подход предусматривает отделение кода. обрабатывающего перечисле­ ние, от критерия отбора. Язык С# облегчает задачу, позволяя передавать функции как объекты. В листинге 4.29 показан единственный расширяющий метод, который фильтрует перечисление объектов Product, но делегирует отдельной функции реше­ ние о том, какие из них включать в результат. Листинr 4.29. Соэдание универсальноrо фильтрующеrо метода в файле МyExtensionМethods. cs из папки Models using System.Collections.Generic; using System; namespace LanguageFeatures.Models puЫic static class MyExtensionMethods static decimal TotalPrices(this decimal total = О; puЫic IEnumeraЬle<Product> products) { 11 О Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 foreach (Product prod in products) total += prod?.Price ?? О; return total; static IEnwneraЫe<Product> Filter ( this IEnwneraЫe<Product> productEnwn, Func<Product, bool> selector) { foreach (Product prod in productEnwn) if (selector(prod)) { yield return prod; puЫic Вторым аргументом метода ект Product кцию для каждого объекта вращает true. Product 4.30. bool. Метод Filter () вызывает эту фун­ и включает его в результат, если функция воз­ Для применения метода автономную функцию (листинг Листинг является функция, которая принимает объ­ Fil ter () и возвращает значение типа Fil ter () можно указать метод или создать 4.30). Использование функции для фильтрации объектов в файле HomeController. cs из папки Product Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using Systeш; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller bool FilterByPrice(Product р) { return (p?.Price ?? 0) >= 20; puЫic ViewResult Index() { Product [] productArray = { "Kayak", Price = 275М}, new Product {Name "Lifejacket", Price = 48.95М}, new Product {Name 19.SOM}, "Soccer ball", Price new Product {Name "Corner flag", Price = 34.95М} new Product {Name 1; Func<Product, bool> nameFilter return prod?.Naшe?[O] == 'S'; = delegate } ; priceFilterTotal = productArray .Filter(FilterByPrice) .TotalPrices(); deciшal naшeFil terTotal = productArray .Filter(nameFilter) .TotalPrices(); deciшal (Product prod) { Глава 4. Важные функциональные возможности языка С# 111 return View("Index", new string[] ( $"Price Total: (priceFilterTotal:C2)", $"Name Total: {nameFilterTotal:C2)" )); FilterByPrice (),за­ Func<Product, bool> устраняет такую Оба подхода не идеальны. Определение методов, подобных соряет определение класса. Создание объекта проблему, но сопряжено с неудобным синтаксисом, который труден для восприятия и сопровождения. Именно эту задачу решают лямбда-выражения, позволяя определять функции более элегантным и выразительным способом, как видно в листинге Листинг 4.31. Использование лямбда-выражений в файле из папки using using using using 4.31. HomeController. cs Controllers Microsoft.AspNetCore.Mvc; System.Collections.Generic; LanguageFeatures.Models; System; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЫic ViewResult Index() { Product [] productArray = ( "Kayak", Price = 275М), new Product (Name "Lifejacket", Price = 48.95М), new Product (Name 19.SOM), "Soccer ball", Price new Product (Name "Corner flag", Price = 34.95М) new Product (Name ) ; decimal priceFilterTotal = productArray .Filter(p => (p?.Price ?? 0) >= 20) .TotalPrices(); productArray decimal nameFilterTotal .Filter(p => p?.Name?[O] = 'S') .TotalPrices(); return View("Index", new string[] $"Price Total: (priceFilterTotal:C2)", $"Name Total: (nameFilterTotal:C2)" )) ; Лямбда-выражения вьщелены полужирным. Параметры выражаются без указания типа, который будет выведен автоматически. Символы => можно читать как "направ­ ляется в" и они связывают параметр с результатом лямбда-выражения. В рассматри­ Product по имени р направляется в результат bool, кото­ true, если значение свойства Price эквивалентно или превышает выражении или если значение свойства Name начинается с буквы S во ваемом примере параметр рый будет равен 2О в первом втором выражении. Такой код работает аналогично отдельному методу и делегату в виде функции, но он короче и для большинства людей легче в восприятии. 112 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Другие формы лямбда-выражений Представлять логику делегата посредством лямбда-выражения вовсе не обязательно. С тем же успехом можно вызвать метод: prod => EvaluateProduct(prod) Если требуется лямбда-выражение для делегата, который имеет несколько параметров, то параметры должны быть заключены в круглые скобки: (prod, count) => prod.Price > 20 && count > О Наконец, если в лямбда-выражении необходима логика, которая требует более одного опе­ ратора, то ее можно реализовать, используя фигурные скобки тором ({ }) и завершая блок опера­ return: (prod, count) => { / / ... несколько операторов return resul t; кода .. . Применять лямбда-выражения в коде не обязательно, но они служат изящным способом выражения сложных функций в читабельной и ясной манере. Вы будете часто встречать лямбда-выражения в примерах, приводимых далее в книге. Использование методов и свойств в форме лямбда-выражений Лямбда-выражения можно применять для реализации конструкторов, методов и свойств. Во время разработки приложений MVC, особенно при написании контролле­ ров, часто появляются методы, которые содержат единственный оператор, выбира­ ющий данные для отображения и представление для визуализации. В листинге приведен код метода действия Index ( ) , 4.32 переписанный в соответствии с таким об­ щим шаблоном. Листинг 4.32. Создание общего шаблона действия в файле из папки using using using using using HomeController.cs Controllers Microsoft.AspNetCore.Mvc; Systern.Collections.Generic; LanguageFeatures.Models; Systern; System.Linq; narnespace LanguageFeatures.Controllers puЫic class HorneController : Controller puЫic iliewResul t Index () { return View(Product.GetProducts() .Select(p => р?.Nаше)); ) Метод действия Index ()получает от статического метода Product. GetProducts () Product и с помощью LINQ строит проекцию значений свойств коллекцию объектов Name. Затем проекция применяется в качестве модели представления для стандарт­ ного представления. В результате запуска примера приложения в окне браузера ока­ жется след,ующий вывод: Kayak Lifejacket Глава 4. Важные функциональные возможности языка С# 11 З В окне браузера также будет присутствовать пустой элемент списка, потому что метод GetProducts () включает в свой результат ссылку null, но в данном разделе главы это неважно. Когда тело конструктора или метода состоит из единственного оператора, его мож­ но переписать в виде лямбда-выражения (листинг Листинг 4.33. Метод действия, представленный как лямбда-выражение, в файле using using using using using 4.33). HomeController. cs из папки Controllers Microsoft.AspNetCore.Mvc; System.Collections.Generic; LanguageFeatures.Models; System; System.Linq; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller ViewResul t Index () => View(Product.GetProducts() .Select(p => p?.Name)); puЫic Лямбда-выражения для методов позволяют опустить ключевое слово return и ис­ пользуют символы=> (направляется в) для ассоциирования сигнатуры метода (вклю­ чая аргументы) с его реализацией. Метод Index (), работает точно так же, как метод из листинга Index () показанный в листинге 4.32, 4.33, но представлен более лаконично. Тот же самый базовый подход можно также применять для определения свойств. В листинге 4.34 демонстрируется добавление в класс Product свойства, ко­ торое использует лямбда-выражение. Листинг 4.34. Представление свойства как лямбда-выражения в файле Product. cs из папки Models namespace LanguageFeatures.Models puЫic class Product { Product(bool stock InStock stock; puЬlic puЫic puЫic puЬlic puЫic puЫic puЫic puЫic = true) string Name { get; set; ) string Category { get; set; "Watersports"; decimal? Price { get; set; ) Product Related { get; set; } bool InStock { get; } bool NameВeginsWi thS => Name? [О] 'S' ; static Product[J GetProducts() Product kayak = new Product { Name = "Kayak", Category = "Water Craft", Price = 275М }; 114 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Product lifejacket = new Product(false) { Narne = "Lifejacket", Price = 48.95М ) ; kayak.Related = lifejacket; return new Product[J { kayak, lifejacket, null ); Использование автоматического выведения типа и анонимных типов Ключевое слово var зания ее типа (листинг позволяет определять локальную переменную без явного ука­ 4.35). Такой прием называется выведекием типа или кеявкой типизацией. Листинг 4.35. Использование выведения типа в файле из папки using using using using using HomeController. cs Controllers Microsoft.AspNetCore.Mvc; Systern.Collections.Generic; LanguageFeatures.Models; Systern; Systern.Linq; narnespace LanguageFeatures.Controllers puЫic class HorneController : Controller ViewResul t Index () { var names = new [] { 11 кауаk", "Lifejacket", "Soccer ball" } ; return View(names); puЬlic Речь идет вовсе не о том, что переменная myVariaЬle не имеет типа; мы всего лишь предложили компилятору самостоятельно вывести тип из кода. Компилятор ис­ следует объявление массива и решает, что он является строковым. Выполнение при­ мера дает следующий вывод: Kayak Lifejacket Soccer ball Использование анонимных типов Комбинируя инициализаторы объектов и выведение типов, можно создавать про­ стые объекты модели представления, которые удобны для передачи данных между контроллером и представлением, без необходимости в определении класса или струк­ туры (листинг 4.36). Глава 4. Важные функциональные возможности языка С# Листинг 4.36. Создание анонимного типа в файле HomeController. cs из папки using using using using using 115 Controllers Microsoft.AspNetCore.Mvc; System.Collections.Generic; LanguageFeatures.Models; System; System.Linq; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller 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М } puЫic } ; return View(products.Select(p Каждый объект в массиве products => p.Name)); является анонимно типизированным. Это не означает, что объекты имеют динамическую приро.цу в том смысле, в каком считают­ ся динамическими переменные JavaScript. Это просто значит, что определение типа будет создано автоматически компилятором. Строгая типизация по-прежнему обеспе­ чивается. Скажем, вы можете получать и устанавливать только те свойства, которые бьии определены в инициализаторе. В результате запуска примера в окне браузера отобразится сле.цующий вывод: Kayak Lifejacket Soccer ball Corner flag Компилятор С# генерирует класс на основе имени и типов параметров в инициа­ лизаторе. Два анонимно типизированных объекта, которые имеют те же самые имена и типы свойств, будут относиться к одному и тому же автоматически сгенерированно­ му классу. В результате все объекты в массиве products получат один и тот же тип, поскольку они определяют те же самые свойства. Совет. Для определения массива анонимно типизированных объектов должно применять­ ся ключевое слово var, т.к. тип не будет создан до тех пор, пока код не скомпилирует­ ся, и его имя не известно. Все элементы в массиве анонимно типизированных объектов обязаны определять те же самые свойства, иначе компилятор не сможет выяснить тип массива. В целях демонстрации изменим вывод в листинге типа вместо значения свойства Name. 4.37, чтобы отображать имя 116 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 4.37. Отображение имени анонимного типа в файле из папки using using using using using HomeController.cs Controllers Microsoft.AspNetCore.Mvc; System.Collections.Generic; LanguageFeatures.Models; System; System.Linq; namespace LanguageFeatures.Controllers puЫic class HomeController Controller ViewResult Index() { var products = new [] { new { Name "Kayak", Price = 275М } , "Lifejacket", Price = 48.95М }, new { Name new { Name "Soccer ball", Price 19.SOM }, new { Name "Corner flag", Price = 34.95М } puЫic }; return View(products.Select(p => p.GetType() .Name)); Всем объектам в массиве был назначен один и тот же тип, в чем можно убедиться. запустив пример. Имя типа не выглядит дружественным к пользователю, но оно и не рассчитано на непосредственное использование. и в вашем случае может оказаться другим: <>f __AnonymousType0'2 <>f AnonymousType0'2 <>f~AnonymousType0'2 <>f AnonymousType0'2 Использование асинхронных методов Асинхронные методы осуществляют возврат и выполняют работу в фоновом режи­ ме с уведомлением о ее завершении, позволяя коду в это время заниматься другими действиями. Асинхронные методы - важный инструмент при устранении узких мест в коде; они позволяют приложениям извлекать преимущества от наличия нескольких процессоров и процессорных ядер. чтобы выполнять работу в параллельном режиме. В инфраструктуре MVC асинхронные методы могут применяться для увеличения общей производительности приложения, предоставляя серверу большую степень гиб­ кости относительно того, как запросы планируются и выполняются. Для выполне­ ния работы асинхронным образом используются два ключевых слова С# - async и await. Преследуемые в настоящем разделе цели требуют добавления в пример проекта новой сборки .NET. чтобы можно было делать асинхронные НТТР-запросы. Щелкнем правой кнопкой мыши на имени проекта выберем в контекстном меню пункт LanguageF'eatures. csproj) LanguageF'eatures в окне Solution Explorer, Edit LanguageFeatures.csproj (Редактировать и добавим элемент. показанный в листинге 4.38. Глава 4. Важные функциональные возможности языка С# Листинг 117 4.38. Добавление пакета в файле LanguageFeatures. csproj из папки LanguageFeatures <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <PackageReference Include="System.Net.Http" Version="4.3.2" /> < /I temGroup> </Project> После сохранения файла сборку в System. Net. Http главе 6. LanguageFeatures. csproj среда Visual Studio загрузит и добавит ее в проект. Более подробно процесс будет описан Работа с задачами напрямую Язык С# и платформа .NET предлагают великолепную поддержку для асинхронных методов, но код быстро становится многословным, а разработчики, не привыкшие к параллельному программированию, зачастую не могут справиться с необычным син­ таксисом. В качестве примера в листинге ни 4.39 приведен асинхронный метод по име­ (),который определен в классе GetPageLength Models в виде папку Листинг 4.39. файла MyAsyncMethods, добавленном в MyAsyncMethods. cs. Содержимое файла МyAsyncМethods. cs из папки Models using System.Net.Http; using System.Threading.Tasks; namespace LanguageFeatures.Models puЫic class MyAsyncMethods { puЫic static Task<long?> GetPageLength() HttpClient client = new HttpClient(); var httpTask = client.GetAsync("http://apress.com"); return httpTask.ContinueWith((Task<HttpResponseMessage> antecedent) => 1 return antecedent.Result.Content.Headers.ContentLength; }) ; Метод использует объект System.Net.Http.HttpClient держимого домашней страницы издательства Apress и для запрашивания со­ возвращает длину полученного содержимого. Работа, которая будет выполняться асинхронно. представлена в .NET Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 118 как объект Task. Объекты Task строго типизируются на основе результата, выдавае­ мого фоновой работой. Таким образом, при вызове метода получается объект Task<HttpResponseMessage>. HttpClient. GetAsync () Он сообщает о том, что запрос вы­ полнится в фоновом режиме и его результатом будет объект Ht tpResponseMessage. Совет. Когда мы употребляем понятия вроде фоновый режим, то опускаем массу деталей, чтобы подчеркнуть только ключевые аспекты, которые важны для мира .NET MVC. Поддержка для асинхронных методов и параллельного программирования в целом превосход­ на, и ее рекомендуется внимательно изучить, если вы хотите создавать по-настоящему высокопроизводительные приложения, которые способны извлекать преимущества от многопроцессорного или многоядерного оборудования. Вы увидите, как MVC облегчает создание асинхронных веб-приложений далее в книге по мере представления разнооб­ разных функциональных средств. Самой непонятной для большинства программистов частью является продолжение, представляющее собой механизм, с помощью которого указывается то, что должно произойти, когда фоновая задача завершится. В приведенном примере применяется метод Con tinueW i th () для обработки объекта Ht tpResponseMessage, получаемого HttpClient. GetAsync (). Это делается с использованием лямбда-выра­ из метода жения, которое возвращает значение свойства, содержащего длину полученного от веб-сервера Apress содержимого. Вот код продолжения: return httpTask.ContinueWith( (Task<HttpResponseMessage> antecedent) => { return antecedent.Result.Content.Headers.ContentLength; 1); Обратите внимание, что ключевое слово return встречается два раза. Именно дан­ return указывает, Task<HttpResponseMessage>. который при завершении задачи возвратит (второе ключевое слово return) длину заголовка ContentLength. Заголовок ContentLength возвращает значение long? (тип long, допускающий null), т.е. результатом метода GetPageLength () является Task<long?>: ная часть вызывает путаницу. Первое применение ключевого слова что возвращается объект puЫic static Task<long?> GetPageLength() { Не переживайте, если все сказанное выглядит для вас бессмысленным замешательстве вы не одиноки. Именно по этой причине в Microsoft - в своем и решили доба­ вить в С# ключевые слова. упрощающие работу с асинхронными методами. Применение ключевых слов Разработчики из Microsoft async и awai t ввели в язык С# два ключевых слова, которые спе­ циально призваны облегчить использование Ht tpClien t. GetAsync () . Ими являются async как с их помощью упростить наш пример метода. асинхронных методов, и awai t; в листинге 4.40 подобных показано, Глава 4. Важные функциональные возможности языка С# Листинг Применение ключевых слов 4.40. из папки async и awai t в файле МyAsyncМethods 119 . cs Models using Systern.Net.Http; using Systern.Threading.Tasks; narnespace LanguageFeatures.Models puЫic class MyAsyncMethods { puЫic async static Task<lonq?> GetPageLength() HttpClient client = new HttpClient(); var httpMessage = await client.GetAsync("http://apress.com"); return httpMessage.Content.Headers.ContentLength; Ключевое слово awai t используется при вызове асинхронного метода. Оно сооб­ щает компилятору С# о том. что необходимо подождать результата возвращается методом GetAsync (), Task, который и затем заняться выполнением остальных опе­ раторов в том же методе. Применение ключевого слова метода GetAsync awai t означает, что мы можем трактовать результат ().как если бы он был обычным методом, и просто присвоить воз­ вращаемый им объект HttpResponseMessage какой-нибудь переменной. Еще лучше то, что затем можно использовать ключевое слово выдачи результата из другого метода - return традиционным образом для ContentLength в данном значения свойства случае. Это намного более естественный подход. к тому же нам не придется беспо­ коиться по поводу метода слова ContinueWi th () и многократного применения ключевого return. При использовании ключевого слова туре метода ключевое слово async, awai t ре. Тип результата метода не изменяется возвращает ализованы Task<long?>. с применением потребуется также добавить к сигна­ как было сделано в рассмотренном выше приме­ - метод GetPageLength () по-прежнему await и async ре­ Причина в том, что ключевые слова ряда искусных трюков компилятора. которые позволя­ ют использовать более естественный синтаксис, но не изменяют того, что происхо­ дит внутри методов, к которым они применены. Программист, вызывающий метод GetPageLength ().по-прежнему имеет дело с результатом существует фоновая операция. которая выдает значение Task<long?>, т.к. все еще long, допускающее null; хотя, конечно же, программист может также предпочесть пользоваться ключевыми словами awai t и async. Такой шаблон повсеместно соблюдается в контроллере МVС, что позволяет легко писать асинхронные методы действий (листинг Листинг 4.41. Определение асинхронных методов действий в файле HomeController. cs using using using using using usinq 4.41). из папки Controllers Microsoft.AspNetCore.Mvc; Systern.Collections.Generic; LanguageFeatures.Models; Systern; Systern.Linq; System.Threadinq.Tasks; narnespace LanguageFeatures.Controllers 120 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 puЫic class HomeController : Controller { async Task<ViewResul t> Index () { long? length = awai t МyAsyncМethods . GetPageLength () ; return View(new string[] { $"Length: {length}" }) ; puЫic 1Ип результата метода действия Index () изменен на Task<ViewResul t>. Подобным образом инфраструктуре МVС сообщается о том. что метод действия будет возвращать объект Task, который по завершении выдаст объект ViewResul t, а тот предоставит детали представления. подлежащего визуализации, и требующиеся ему данные. К оп­ ределению метода было добавлено ключевое слово вать ключевое слово awai t при вызове метода О продолжениях позаботятся инфраструктура async, что позволило использо­ MyAsyncMethods. GetPathLength (). MVC и платформа .NET. а результатом будет код, который легко писать. читать и сопровождать. После запуска приложения появится вывод. похожий на показанный ниже (хотя наверняка с отличающейся дли­ ной, т.к. содержимое веб-сайта Apress часто изменяется): Length: 54576 Получение имен Во время разработки веб-приложений есть много задач. в которых необходимо ссы­ латься на имя аргумента, переменной. метода или класса. Распространенными при­ мерами может служить генерация исключения или создание ошибки проверки досто­ верности при обработке пользовательского ввода. Традиционный подход предполагал применение строкового значения с жестко закодированным именем (листинг Листинг 4.42. Использование жестко закодированного имени в файле HomeController. св using using using using using 4.42). из папки Controllers Microsoft.AspNetCore.Mvc; System.Collections.Generic; LanguageFeatures.Models; System; System.Linq; namespace LanguageFeatures.Controllers puЬlic class HomeController : Controller puЫic ViewResult Index() ( var products = new [] { new { Name = "Кауаk", Price = 275М } , new { Name = "Lifejacket", Price = 48. 95М } , new { Name = "Soccer ball", Price = 19.SOM } , new { Name = "Corner flag", Price = 34. 95М } }; return View (products. Select (р => $ "Name: Вызов метода Select () из LINQ {р. Name} , Price: {р. Price} ") ) ; генерирует последовательность строк. каждая из которых содержит жестко закодированную ссылку на свойства Name и Price. Глава 4. Важные функциональные возможности языка С# 121 Запуск приложения дает следующий вывод в окне браузера: Name: Name: Name: Name: Kayak, Price: 275 Lifejacket, Price: 48.95 Soccer ball, Price: 19.50 Corner flag, Price: 34.95 Проблема такого подхода в том, что он предрасположен к ошибкам, которые обусловлены либо неправильно набранным именем, либо некорректно обновленным именем в строке после рефакторинга. Результат может вводить в заблуждение, что особенно проблематично для сообщений, отображаемых пользователю. Язык С# под­ держивает выражение nameof, благодаря которому ответственность за формирова­ ние строки имени возлагается на компилятор (листинг Листинг 4.43. Использование выражений из папки 4.43). nameof в файле HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using LanguageFeatures.Models; using System; using System.Linq; namespace LanguageFeatures.Controllers puЫic class HomeController : Controller puЫic ViewResult Index() { var products = new [] { "Kayak", Price = 27 5М } , new { Name "Lifejacket", Price = 48.95М }, new { Name 19. 50М } , "Soccer ball", Price new { Name "Corner flag", Price = 34.95М} new { Name }; return View(products.Select(p => $"{nameof(p.Name)}: {p.Name}, {nameof(p.Price)}: {p.Price}")); Компилятор обрабатывает ссьmку вроде р . Narne таким образом, что в строку вклю­ чается только последняя часть, порождая тот же самый вывод. как и в предыдущем при­ мере. Среда narneof, Visual Studio располагает поддержкой средства IntelliSense для выражений поэтому вы будете снабжены подсказками при выборе ссьmок, а выражения корректно обновятся в случае рефакторинга кода. Поскольку за работу с narneof отвеча­ ет компилятор, применение недопустимой ссьmки приводит к ошибке на этапе компиля­ ции, что не позволит некорректным или устаревшим ссьmкам ускользнуть от глаз. Резюме В главе бьm дан обзор основных языковых средств С#, которые должен знать резуль­ тативный программист приложений МVС. Язык С# достаточно гибкий для того, чтобы обычно существовало несколько способов решения любой задачи, но есть средства, с которыми вы будете чаще всего встречаться во время разработки веб-приложений и видеть повсюду в примерах. рассматриваемых в данной книге. В следующей главе бу­ дет представлен механизм визуализации Razoг и приведены объяснения, как его ис­ пользовать для генерации динамического содержимого в веб-приложениях МVС. ГЛАВА 5 Работа с Razor в приложении ASP.NET Core MVC для выпуска содержимого, отправляемого кли­ ентам, используется компонент, который называется механизмом визуали­ за41ш. Стандартным механизмом визуализации является Razor, и он обрабатывает аннотированные НТМL-файлы, производя поиск инструкций, которые вставляют ди­ намическое содержимое в вывод. отправляемый браузеру. В настоящей главе дается краткое введение в синтаксис опознавать выражения Razor, Razor, так что вы сможете когда столкнетесь с ними. Мы не собираемся превра­ Razor; считайте ее в большей степени Razor будут раскрываться в после­ других средств MVC. В табл. 5.1 приведена щать главу в исчерпывающий справочник по ускоренным курсом по синтаксису. Особенности дующих главах книги при рассмотрении сводка, позволяющая поместить Таблица 5.1. Помещение Razor Razor в контекст. в контекст Вопрос Ответ Что это такое? Razor - это механизм визуализации, отвечающий за встраива­ ние данных в документы Чем он полезен? HTML Возможность динамической генерации содержимого является неотъемлемой частью процесса написания веб-приложения. Механизм визуализации Razor предоставляет средства, которые упрощают работу с остальной инфраструктурой MVC Как он используется? ASP.NET Core с применением операторов С# Выражения Razor добавляются к статической НТМL-разметке в файлах представлений. Такие выражения оцениваются для гене­ рации ответов на клиентские запросы Существуют ли какие-то Выражения скрытые ловушки или При решении, должна логика принадлежать представлению или ограничения? контроллеру, иногда возникают затруднения, и это способно Razor могут содержать почти любые операторы С#. разрушить принцип разделения обязанностей, который является центральным в паттерне MVC Существуют ли В главе альтернативы? зуализации. Доступны механизмы визуализации от независимых 21 объясняется, как написать собственный механизм ви­ поставщиков, но они обычно полезны только в ограниченных си­ туациях и для них не гарантируется долгосрочная поддержка В табл. 5.2 приведена сводка по главе. Глава 5. Работа с Razor Таблица 123 5.2. Сводка по главе Листинг Задача Решение Доступ к модели представления Используйте выражение @model для оп­ ределения типа модели и выражения @Model для 5.5, 5.14, 5.17 доступа к объекту модели Использование имен типов без их Создайте файл импортирования уточнения с помощью пространств представлений 5.6, 5.7 имен Применяйте компоновку 5.8-5.1 О Указание стандартной компоновки Используйте файл запуска представления 5.11-5.1 З Передача данных из контроллера Применяйте объект 5.15, 5.16 Определение содержимого, которое будет использоваться множеством представлений ViewBag представлению за пределами модели представления Выборочная генерация содержимого Используйте условные выражения Генерация содержимого для каждого Применяйте выражение элемента в массиве или коллекции низма Razor @foreach меха- 5.18, 5.19 5.20, 5.21 Razor Подготовка проекта для примера Razor создадим новый проект ASP.NET Core Web Application (Неб-приложение ASP.NET Core) из группы .NET Core, используя шаблон Empty (Пустой). как поступали в предыдущей главе. Включим инфраструктуру MVC, внеся в класс Startup изменения. которые по­ Для демонстрации работы механизма визуализации по имени Razor типа казаны в листинrе Листинг using using using using using using using using 5.1. 5.1. Включение MVC в файле Startup. cs из папки Razor System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace Razor { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); puЫic void Configure(IApplicationBuilder { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); арр, IHostingEnvironment env) 124 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 // app.Run(async (context) => { // awai t context. Response. Wri teAsync ( "Hello World ! ") ; // }) ; app.UseМvcWithDefaultRoute(); Определение модели Создадим папку Models веденным в листинге Листинг 5.2. 5.2 и добавим в нее файл класса по имени Product. cs с при­ определением простого класса модели. Содержимое файла Product. cs из папки Models namespace Razor.Models { puЫic class Product { puЬlic puЫic puЫic puЫic puЫic int ProductID { get; set; string Name { get; set; } string Description { get; set; decimal Price { get; set; string Category { set; get; } Создание контроллера Стандартная конфигурация, установленная в файле Startup. cs, следует соглаше­ Home no умолчанию. класса HomeController. cs, по­ нию МVС относительно отправки запросов контроллеру с именем Создадим папку Controllers и добавим в нее файл местив в него простое определение контроллера (листинг Листинг 5.3. Содержимое файла HomeController. cs using Microsoft.AspNetCore.Mvc; using Razor.Models; namespace Razor.Controllers { puЫic class HomeController : Controller ViewResult Index() { Product myProduct = new Product { ProductID = 1, Name = "Kayak", Description = "А boat for one person", Category = "Watersports", Price = 275М puЫic }; return View(myProduct); 5.3). из папки Controllers Глава 5. Работа с Razor В контроллере определен метод действия по имени ся объект View ( ) , Product с заполнением его свойств. Объект 125 I ndex (), в котором создает­ Product передается методу так что он используется как модель во время визуализации представления. При вызове метода View () имя файла представления не указывается, поэтому будет применяться стандартное представление для метода действия. Создание представления Чтобы определить стандартное представление для метода действия Index () , здадим папку ставления Листинг Views / Home и добавим в нее файл типа MVC View Page (Страница MVC) по имени Index. cshtml с содержимым из листинга 5.4. со­ пред­ 5.4. Содержимое файла Index. cshtml из папки Views/Home @model Razor.Models.Product @{ Layout = null; < 1 DOCTYPE html > <html> <head> <meta name="viewport" con ten t ="width=de vice-w i dth " /> <title>Inde x</t it l e> </head> <body> Con tent will go here </ body> </ html> В последующих разделах мы рассмотрим различные части представления Razor и продемонстрируем разнообразные вещи. которые с ним можно делать. При изуче­ нии Razor полезно помнить. что представления существуют для выражения пользо­ вателю одной или более частей модели - и это оэначает генерацию НТМL-разметки , которая отображает данные, извлеченные из одного или множества объектов. Если не забывать, что мы всегда пытаемся строить НТМL-страницу. которая может быть отправлена клиенту. тогда вся активность механизма визуализации Razor обретает смысл. В результате запуска приложения отобразится простой вывод, приведенный на рис. 5.1. Content \Yill go 11ere Рис. 5. 1. Выполнение примера приложения 126 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Работа с объектом модели Давайте начнем с самой первой строки в файле представления I n dex . c shtml : @mo del Razor .Mo dels. Pr od u c t Выражения Razor начин аются с символа @. В да нном случае выражение @mo d e 1 объявляет тип объекта модели. который будет пер едаваться представлению из ме ­ тода действия . В итоге появляется возможность ссылки на методы , поля и сво йства объекта модели представления посредством @Model , отображается простое дополнение к представлению Листинг 5.5. ка к показано в листинге 5.5, где I nde x . Ссылка на свойство объекта модели представления в файле Index. csh tml из папки Views /Home @model Ra z o r . Models.Prod u c t @{ Layout = nul l ; < 1 DOCTYPE html> <htm l > <h e ad> <me t a name= " v i ewp or t" co n t e n t= " wi dth =dev i ce-wi dth " /> <t itle>Index< / t i tle > </ h e ad> <bo dy > @Model.Name </ b o dy > </ h tml > На заметку! Обратите внимание, что тип объекта модели представления объявляется с ис­ пользованием применением @mo de l @Model (со строчной буквой m), а доступ к свойству Name производится с (с прописной буквой М). Поначалу такая особенность может немно­ го запутывать , но со временем она станет вполне привычной. П осле запуска прил ожения отобразится вывод . представленный н а рис . С \ Q ~ca~hos :60753 Kayak Рис. 5.2. ========= Результат чтения значения свой ства внутри пр е дста вл е н ия 5.2. Глава 5. Работа с Razor Представление. которое использует выражение 127 @mode l для указания типа, назы­ вается строго типизированным представлением. Среда Visнal Studio способна при­ @model для открытия окна со списком предполагаемых нов в случае ввода @Model с последующей точкой (рис . 5.3). менять выражение имен чле­ ""°del Rozor .~lodels . Product 3 4 Loyout • null; 5 б 7 8 9 10 11 12 < ! ООСТУРЕ ht ml> B <html> G <heod> <met l!I name•"viewport"' content•"width•device width" /> <ti tle> Index</ti tle> </head> 13 .Э <Ьоdу> l 4 @'l'lodel.~ ~ </body> 14 15 16 17 </html> ~ Otscription ... Ф Equ•ls Ф GetHA<hCodc GetType Ф ~ · 1· -.'to-··-----·~ ProductlO Price 1100 8 .... string Razor.Models.Product.Nome { get; set; } ~ Ф Рис. 5.3. Среда ToString Visual Studio предлагает список предполагаемых имен членов на основе выражения @Mode l Список предполагаемых имен членов, отображаемый Visual Studio, помогает из­ бегать ошибок в представлениях Razoг. При желании такие предположения можно игнорировать, и тогда Visual Studio будет подсвечивать проблемные имена членов, чтобы можно было внести исправления. как поступает с обычными файлами классов 5.4, где мы пытается сослать­ Visual Sttidio обнаруживает, что С#. Пример подсветки проблемы можно видеть на рис. ся на свойство класс @Model. NotARealP roper t y . Среда Product , указанный в качестве типа модели, не содержит данное свойство, и подсвечивает ошибку в редакторе. l) . r. Г::J <head> <meta name="vie1~port" content="width=device-1vidth" /> <title>Index</title> </head> t - <body> @Мodel . NotARealPro er f </Ьоdу> </html> 1 Рис. 5.4. Среда Visual Studio сообщает о проблеме с выражением @Mode l 128 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Использование файла импортирования представлений При определении объекта модели в начале файла Index. cshtml необходимо было включать пространство имен, которое содержит класс модели. например: @model Razor.Models.Product По умолчанию все типы. на которые производится ссылка в строго типизирован­ ном представлении Razor. должны уточняться своими пространствами имен. Это не особо крупная проблема. когда есть только ссылка на объект модели. но понимание представления может значительно затрудниться при написании более сложных вы­ ражений Razoг. таких как рассматриваемые позже в главе. Добавив в проект файл импорmuрования представлений. можно указать набор про­ странств имен. в которых должен осуществляться поиск типов. Файл импортирования представлений размещается в папке На заметку! Файлы в папке ( ), Views, Views и имеет имя_ Viewlmports. cshtml. имена которых начинаются с символа подчеркивания пользователю не возвращаются, что позволяет с помощью имен файлов проводить различие между представлениями, подлежащими визуализации, и поддерживающими их файлами . Файлы импортирования представлений и файлы компоновки (будут описаны вскоре) имеют имена, начинающиеся с символа подчеркивания. Чтобы создать файл импортирования представлений, щелкнем правой кнопкой мыши на папке Addc~New ltem Views в окне Solution Explorer. (Страница импортирования представлений (рис. Sortby: jD!f1ult ASP.NEТ Core Г,}." ~ Code Gentr11I .., Web ASP.NEТ Generol Scripts Content ~ MVC) MVC View lmports Page ASP.NET CoreqWeb из категории 5.5). .., lnstolled .., выберем в контекстном меню пункт (ДобавитьqНовый элемент) и укажем шаблон ASP . NEТ MVCViewlmport<P•ge ASP.NEТ Core ~· R.iorT19 Helper ASP . NEТ Core ~· Middle>vare Class ASP.NEТ Core ~· Stortup cl•ss ASP. NEТ Core ASP . NEТ ASP . NEТ Core ASP . NEТ Core vГJ rш о Configuration File Razor P1ge _Viewlmpoot<.cshtml 5.5. '"' Туре: МVCVi [ij ~ Рис. Core ASP.NET Core ~ №me MVC View Layout P•ge ;;· , _ ~· МVС View St•rt P1ge ~ Online -----~ Создание файла импортирования представлений 129 Глава 5. Работа с Razor Visual Studio автоматически назначит файлу имя_Viewimports. cshtml, а Add (Добавить) приведет к созданию файла, который будет пустым. Поместим в файл выражение, показанное в листинге 5.6. Среда щелчок на кнопке Листинг 5.6. Содержимое файла_Viewimports. cshtml из папки Views @using Razor.Models Пространства имен, где должен проводиться поиск классов, применяемых в пред­ ставлениях Razor, указываются с использованием выражения следует пространство имен. В листинге Razor. Models, 5.6 @u s i n g, содержащего класс модели в примере приложения. Теперь, когда пространство имен Razor. Models включено в файл импортирова­ ния представлений, можно удалить пространство имен из файла тинг за которым добавлена запись для пространства имен Index. cshtml (лис­ 5.7). Листинг 5. 7. Пропуск пространства имен с классом модели в файле из папки Index. csh tml Views/Home @model Product @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> @Model.Name </body> </html> Совет. Выражение @using можно также добавлять в отдельные файлы представлений, что позволит применять внутри них типы без указания пространства имен. Работа с компоновками Ниже приведено еще одно важное выражение Razor из файла представления Index. cshtml: @{ Layout null; Это пример блока кода Razor, который позволяет включать в представление опе­ раторы С#. Блок кода открывается посредством @{ и закрывается с помощью содержащиеся в нем операторы оцениваются при визуализации представления. }, а 130 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Показанный выше блок кода устанавливает значение свойства Представления Razor La you t в n u 11 . MVC. а в ба­ Layout . В главе 21 объ­ компилируются в классы С# внутри приложения зовом классе, который они используют. определено свойство ясняется, как все это работает, а пока достаточно знать, что результатом установки свойства Layout в null является сообщение инфраструктуре MVC о том , что наше представление является самодостаточным и будет визуализировать все свое содержи­ мое, которое необходимо возвратить клиенту. Самодостаточные представления хороши для простых примеров приложений. но реальный проект может включать десятки представлений. и некоторые из них будут иметь общее содержимое. Дублированное общее содержимое в представлениях стано­ вится трудным в управлении. особенно если нужно внести изменение и отследить все представления, подлежащие модификации. Более эффективный подход предусматривает использование компоновки Razor, яв­ ляющейся шаблоном, который хранит общее содержимое и может быть применен к одному или большему числу представлений. Когда вы вносите изменение в компонов­ ку. оно автоматически воздействует на все представления, которые ее используют. Создание КОМПОНОВКИ Компоновки обычно совместно используются представлениями. применяемыми множеством контроллеров, и хранятся в папке по имени входит в перечень местоположений, просматриваемых Чтобы создать компоновку, создадим папку Views / S h ared , которая Razor в попытках найти файл. Vi e ws/Share d. щелкнем на ней правой кнопкой мыши и выберем в контекстном меню пункт Add9New ltem (Добавить9Новый MVC View Layout Page (Страница компоновки представле­ ASP.NET Core9Web и введем_ BasicLayou t . csh tml в качес­ (рис. 5.6). Щелкнем на кнопке Add (Добавить) для создания файла. элемент). Укажем шаблон ний MVC) в категории тве имени файла (Подобно файлам импортирования представлений имена файлов компоновок начина­ ются с символа подчеркивания.) _. lnstalled _. ASP.NET Core Cod• General ASP.NEi Core ~· W•b АР! Controllor Class ASP.NEi Cor• &i" MVC General View Р •9• ~ : MVC Vi•w layout Pag• r;,,s,• ~ MVC View Start Page Scripts Content Online Name: MVC Controll•r Class ~ _. Web ASP.NEi ~ ~· ~ 5.6. Coro ASP.NE.Т Cor• дSP.NEi Core MVC Viow lmports Page ASP.NEТ Core Razor Т•9 H•lper ASP.NEТ Core Middleware Class ASP.NEТ Core _Basicl•yout.cshtml Рис. ASP.NEТ Создание компоновки Глава 5. Работа с Razor В листинге 131 5.8 показано начальное содержимое файла_BasicLayout. cshtml, до­ Visual Studio при создании файла. бавленное средой Листинг Начальное содержимое файла 5.8. из папки _ BasicLayout. cshtml Views/Shared <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body> <div> @RenderBody () </div> </body> </html> - Компоновки это специализированная форма представлений; в листинге присутствуют два выражения@. Вызов метода @RenderBody () 5.8 вставляет в разметку компоновки содержимое представления, указанное с помощью метода действия: <div> @RenderBody () </div> Второе выражение ViewBag. Ti tle Razor в компоновке обращается для установки содержимого элемента к свойству по имени title: <title>@ViewBag.Title</title> Объект ViewBag - удобное средство, которое позволяет передавать значения данных внутри приложения (в рассматриваемом случае между представлением и его компоновкой). Вы увидите, как он работает, когда компоновка будет применена к представлению. Элементы НТМL в компоновке будут применены к любому представлению, которое использует компоновку, предоставляя шаблон для определения общего содержимого. В листинге 5.9 к компоновке добавлена простая разметка. чтобы ее эффект как шаб­ лона был очевидным. Листинг 5.9. Добавление содержимого в файл _ВasicLayout.cshtml из папки Views/Shared <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <style> 132 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 #mainDiv { padding: 20рх; border: solid medium font-size: 20pt Ыасk; </style> </head> <body> <hl>Product Information</hl> <div id="mainDiv"> @RenderBody () </div> </body> </html> Здесь был добавлен элемент заголовка, а также стиль держимого элемента di v, ради прояснения того, в котором находится выражение CSS для стилизации со­ @RenderBody (), какое содержимое поступает из компоновки, просто а какое - из представления. Применение компоновки Чтобы применить компоновку к представлению, понадобится установить значение свойства Layout и удалить НТМL-разметку, которая теперь будет предоставляться компоновкой, такую как элементы Листинг h tml, head и body (листинг 5.10). 5.1 О. Применение компоновки в файле Index. cshtml из папки Views/Home @model Product @( Layout = 11 _BasicLayout 11 ; ViewBag. Т i tle = 11 Product Name 11 ; Product Name: @Model, Name Свойство Layout указывает имя файла компоновки, который будет использовать­ ся для представления, без расширения . cshtml. Механизм Razor будет искать ука­ /Views/Home и Views/Shared. Кроме того, установлено свойство ViewBag. т i tle, которое будет применять­ компоновкой для определения содержимого элемента ti tle при визуализации занный файл компоновки в папках ся представления. Трансформация представления значительна даже для такого простого приложе­ ния. Компоновка располагает всей структурой, требуемой для любого НТМL-ответа, оставляя представлению только заботу о динамическом содержимом, которое отоб­ ражает данные пользователю. Index. csh tml, она ответа (рис. 5. 7). Когда инфраструктура MVC обрабатывает файл применяет компоновку для создания унифицированного НТМL­ Глава 5. Работа с Razor D Product Name С 133 )( <D localhost·60753 P1·oduct Information P1·oduct _____ Nаше: Kayak 1 .____ , _,J Рис. 5.7. Результат применения компоновки к представлению Использование файла запуска представления Осталась еще одна мелкая шероховато сть. которую необходимо устранить - мы должны указывать файл компоновки для применения в каждом представлении . Следовательно. если файл компоновки переименовывается, тогда придется отыскать все ссылающиеся на него представления и внести необходимое изменение , что будет процессом, чреватым ошибками, и противоречит лейтмотиву, который пронизывает всю разработку приложений легкости сопровождения. MVC - Решить упомянутую проблему можно с использованием файла запуска представ· ления. При визуализации представления инфр аструктура ViewStart. cshtml. MVC ищет ф айл по имени Содержимое этого файла будет трактоваться так, как если бы оно присутствовало внутри самого файла представления. и данное средство можно применять для автоматической установки свойства Layo ut. Чтобы создать файл запуска представления. щелкнем правой кнопкой мыши на папке Views. выберем в контекстном меню пункт элемент) и укажем шаблон категории MVC View Start Page ASP.NET CoreqWeb (рис. 5.8). " ASP.Nrт Core Code General • Wd> AddQNew ltem r:f.' L\i::J MVC V1ew L<yout P•ge ~ MVC View St•rt Ра~~ General ~· " MVC View lmports P•g• ASP.NET Core Scripts Content ~ ~- R11zorTag Helper ASP.NEТ ~!>;: Middleware Clas.s ASP .NЕТ Сore ASP.NП t> Online Рис. 5.8. ASP.NET Core , : , дSР.NЕТ Core Core ~ ~· St•rtup сlш !>;: ASP.NEТCore vfJ ASP.NEТ Core ASP.NEТ Core [;§' (ДобавитьqНовый (Файл запуска представления ASP.NП Configur•t1on File Razor Page Создание файла запуска представления ~ ( MVC) в 134 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Среда Visual Studio щелчок на кнопке Add автоматически назначит файлу имя мым, приведенным в листинге Листинг 5.11. ViewStart. cshtml; (Добавить) приведет к созданию файла с начальным содержи­ 5. 11. Начальное содержимое файла Viewstart. cshtml из папки Views @{ Layout " Layout"; Дт:Iя применения компоновки ко всем представлениям в приложении понадобится изменить значение, присваиваемое свойству Листинг 5.12. Layout (листинг 5.12). Применение стандартного представления в файле_ViewStart. cshtml из папки Views @{ Layout "_BasicLayout"; Поскольку файл запуска представления содержит значение для свойства можно удалить соответствующее выражение из файла Листинг 5.1 З. Index. cshtml Применение файла запуска представления в файле из папки Layout, 5.13). (листинг Index. cshtml Views/Home @model Product @{ ViewBag.Title = "Product Name"; Product Name: @Model.Name Специально указывать, что должен применяться файл запуска представления, не требуется. Инфраструктура MVC автоматически обнаруживает данный файл и ис­ пользует его содержимое. Преимущество отдается значениям, определяемым в фай­ ле представления, что делает переопределение файла запуска представления очень простым. Можно также применять несколько файлов запуска представлений, чтобы устано­ вить стандартное поведение для разных частей приложения. Механизм Razor ищет самый близкий файл запуска для обрабатываемого представления, а :это значит, что стандартные настройки можно переопределять, добавляя файл запуска представле­ ния в папку Views/Home или Views/Shared. Внимание! Важно понимать разницу между отсутствием свойства ставления и его установкой в null. вы не хотите применять компоновку, тогда установите свойство просто опустить свойство Layout, Layou t в файле пред­ Если представление является самодостаточным, и то инфраструктура MVC Layout в null. Если же будет считать, что компоновка вам необходима, и она должна использовать значение, которое найдет в файле запуска представления. Глава 5. Работа с Razor Использование выражений 135 Razor Теперь, когда вы ознакомились с основами представлений и компоновок, давай­ те переключимся на другие виды выражений, поддерживаемые Razor, и посмотрим, как их применять для создания содержимого представлений. В хорошем приложении MVC имеется четкое разделение между ролями метода действия и представления. Роли просты и кратко описаны в табл. Таблица 5.3. 5.3. Роли, исполняемые методом действия и представлением Чего не делает Компонент Что делает Метод действия Передает представлению объект Не передает представлению модели представления сформатированные данные Представление Использует объект модели пред­ Не изменяет ни одного аспекта в ставления для отображения со­ объекте модели представления держимого пользователю Далее в книге мы будем неоднократно возвращаться к данной теме. Чтобы измечь максимум из МVС, необходимо соблюдать и обеспечивать разделение между разными частями приложения. Как будет показано, механизм Razor позволяет делать очень мно­ гое, включая применение операторов С#, но вы не должны использовать Razor для вьmол­ нения бизнес-логики или манипулирования объектами моделей предметной области. В листинге Листинг 5.14 демонстрируется 5.14. добамение нового выражения в предстамение Добавление выражения в файле Index. Index. cshtml из папки Views/Home @model Product @{ ViewBag.Title = "Product Name"; <p>Product Name: @Model.Name</p> <p>Product Price: @($ 11 {Model. Price: С2} ") </р> Значение свойства Price можно было бы сформатировать в методе действия и пе­ редать его представлению. Такой подход работает, но разрушает преимущество шаб­ лона MVC и сокращает возможность реагировать на изменения в будущем. Как уже упоминалось, позже мы еще возвратимся к этой теме, но вы должны запомнить, что инфраструктура MVC ASP.NET Core MVC не принуждает надлежаще применять паттерн и нужно учитывать влияние решений, принимаемых во время проектирования и написания кода. Обработка или форматирование данных Важно отличать обработку данных от их форматирования. Представления форматируют данные, поэтому в предыдущем разделе представлению передается объект Product, а не отображаемая строка с форматированными свойствами объекта. За обработку данных, включая выбор объектов данных для отображения, отвечает контроллер, который будет об­ ращаться к модели с целью получения и модификации требующихся данных. Иногда трудно понять, где проходит черта между обработкой и форматированием, но в качестве эмпири­ ческого правила рекомендуется занять осторожную позицию и выносить из представления в контроллер все кроме простейших выражений. 136 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Вставка значений данных Самое простое, что можно делать с помощью выражения Razor - вставлять зна­ чения данных в разметку. Наиболее распространенный способ пре.цусматривает ис­ пользование выражения @Model. Представление Index уже содержит примеры при­ менения такого подхода вроде показанного ниже: <p>Product Name: @Model.Name</p> Вставлять значения можно таюке с использованием объекта рый применялся для установки содержимого элемента ViewBag ViewBag. кото­ в компоновке. Объект можно использовать для передачи данных из контроллера в представление, дополняющее модель (листинг Листинг ti tle 5.15. Применение из папки 5.15). ViewBag в файле HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using Razor.Models; namespace Razor.Controllers puЫic class HomeController : Controller ViewResult Index() { Product myProduct = new Product { ProductID = 1, Name = "Kayak", Description = "А boat for one person", Category = "Watersports", Price = 275М puЬlic }; ViewBag. StockLevel = 2; return View(myProduct); Свойство ViewBag возвращает динамический объект, который может использо­ ваться для определения произвольных свойств. В листинге установлено в 2 свойство по имени StockLevel. 5.15 бьmо определено и Так как свойство ViewBag является динамическим. объявлять имена свойств заранее необязательно, но это означает, что для свойств ViewBag среда Visual Studio не в состоянии предоставлять предполагае­ мые варианты автозавершения. Знание того. когда применять ViewBag, а когда расширять модель - вопрос опы­ та и сложившихся предпочтений. Мой персональный стиль заключается в том, что­ бы использовать объект ViewBag только для предоставления визуальных подсказок о способе визуализации данных и не применять его для значений данных, которые отображаются пользователю. Но это просто то, что подходит лично мне. Если вы хо­ тите использовать ViewBag для данных, отображаемых пользователю, тогда обращай­ тесь к значениям с помощью выражения @ViewBag (листинг 5.16). Глава 5. Работа с Razor Листинг 5.16. Отображение значения из папки 137 ViewBag в файле Index. csh tml Views/Home @mo del Pr odu c t @{ ViewBa g . Ti t le "Pr od uct Name"; = <p> Produc t Name : @Model.Name</p> <p> Produc t Pr ic e: @($"{Model.Pri ce:C2 }")</p> <p>Stock Level : @ViewBag . StockLevel</p> Результат можно видеть на рис. )( D ProdU<! ~•m• С 5.9. Ф localhost бD?S' Product Information Prodttct Naine: Kayak Pгoduct P1·ice: ±:275.00 Stock Level: 2 Рис. 5.9. Применение выражений Razor _J для вставки значений данных Установка значений атрибутов Во всех рассмотренных до сих пор примерах устанавливалось содержимое элемен­ тов. но выражения Razor можно использовать также для установки з начений бurnoв элемента . В листинге @ViewBa g Листинг 5.17 для установки содержимого атрибутов в эл е ментах 5.17. Установка значений атрибутов в файле из папки ampu@Mod el и представления Index . демонстрируется применение выражений Index. csh tml Views/Home @mo del Pr oduct @{ ViewBag.Tit l e = " Pr oduct Name "; <div data-productid="@Мodel.ProductID" data-stocklevel="@Viewвag.StockLevel"> <p>Product Name: @Mode l . Name</p > <p> Pr oduc t Pr i ce : @($ "{Mode l .Pri ce : C2} " )</p> <p>Stoc k Level : @ViewBag . St oc kLeve l </p> </div> 138 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Выражения Razor данных в элементе применяются для установки значений некоторых атрибутов di v. Совет. Атрибуты данных, которые представляют собой атрибуты с именами, начинающимися на da t.a - , в течение многих лет были неформальным способом создания специальных HTML5. Чаще всего они ис­ пользуются так, что код JavaScript может находить специфические элементы, или так, что стили CSS могут применяться более ограниченным образом. атрибутов и затем сделаны частью формального стандарта Если запустить пример приложения и просмотреть НТМL-разметку, отправленную браузеру, то легко увидеть, как механизм Razor установил значения атрибутов: <div data-productid="l" data-stocklevel="2"> <p>Product Name: Kayak</p> <p>Product Price: $275.00</р> <p>Stock Level: 120</р> </div> Использование условных операторов Механизм Razor способен обрабатывать условные операторы, что означает воз­ можность настройки вывода из представления на основе значений данных представ­ ления. Такой прием является наиболее важной частью сложные и изменчивые компоновки. которые понимании и сопровождении. В листинге представления Листинг Index, 5. 18 Razor по-прежнему и позволяет создавать остаются простыми в приведено обновленное содержимое включающее условный оператор. 5.18. Применение условного оператора Razor в файле Index. csh tml из папки Views/Home @model Product @{ ViewBag.Title = "Product Name"; <div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel"> <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @switch (ViewBag.StockLevel) case О: @: Out of Stock break; case 1: case 2: case 3: <b>Low Stock (@ViewBag.StockLevel)</b> break; default: @: @ViewBag.StockLevel in Stock break; </р> </div> Глава 5. Работа с Razor 139 Чтобы начать условный оператор. нужно поместить символ @ перед условным клю­ чевым словом С# (swi tch в текущем примере). Блок кода завершается закрывающей фигурной скобкой (})подобно обычному блоку кода С#. Внутри блока кода Razor в вывод представления можно включать НТМL-элементы и значения данных, просто определяя НТМL-разметку и выражения Razor: <b>Low Stock (@ViewBag.StockLevel)</b> Помещать элементы или выражения в кавычки либо отмечать их каким-то спе­ циальным образом не требуется - механизм Razor будет интерпретировать это как вывод, подлежащий обработке. Тем не менее, если нужно вставить в представление литеральный текст. когда он не содержится в НТМL-элементе, тогда о данном факте понадобится сообщить @: Razor. добавив к строке префикс: Out of Stock Символы @: предотвращают обработку механизмом Razor строки как оператора С#, что является стандартным поведением в отношении текста. Результат выполне­ ния такого условного оператора показан на рис. С 5.10. - Ф localhost:60753 Product Information Ргоdнсt Nаше: Ргоdнсt Kayak Price: f.275.00 Stock Level: Lo\-v Stock (2) L_.____________. Рис. 5.1 О. Применение оператора sw i tch в представлении Условные операторы играют важную роль в представлениях Razor Razor. поскольку они позволяют варьировать содержимое на основе значений данных, которые представ­ ление получает от метода действия. В качестве дополнительной демонстрации в лис ­ тинге 5.19 приведено представление I ndex. csh tml с добавленным оператором еще одним распространенным условным оператором. i f - 140 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 5.19. Использование оператора if в файле Index.cshtml из папки Views/Home @model Product @{ ViewBag.Title = "Product Name"; <div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel"> <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @if (ViewBaq.StockLevel == О) { @: Out of Stock else if (ViewBaq. StockLevel > О && ViewBaq. StockLevel <== 3) { <b>Low Stock (@ViewBaq.StockLevel)</b> else { @: @ViewBaq.StockLevel in Stock </р> </div> Условный оператор if выдает те же самые результаты, что и оператор switch, но мы просто хотели продемонстрировать применение условных операторов С# в пред­ ставлениях Razor. Дополнительные 21, где представления найти в главе сведения о работе условных операторов можно рассматриваются более подробно. Проход по содержимому массивов и коллекций При разработке приложения MVC часто необходимо выполнять проход по содер­ жимому массива или другой разновидности коллекции объектов с генерацией подроб­ ной информации для каждого объекта. Чтобы продемонстрировать. как это делается. в листинге Home, 5.20 показан модифицированный метод действия Index () в контроллере Product. который теперь передает представлению массив объектов Листинг 5.20. Использование массива в файле из папки HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using Razor.Models; namespace Razor.Controllers { puЫic class HomeController : Controller IActionResult Product[] array new Product {Name new Product {Name new Product {Name new Product {Name puЬlic ={ }; return View(array); Index() { = = = = "Kayak", Price = 275М), "Lifejacket", Price = 48.95М}, "Soccer ball", Price = 19. SOM}, "Corner flaq", Price = 34. 95М} Глава 5. Работа с Razor Метод действия Inde x ( ) создает объект значения данных, и передает его методу стандартного представления. В листинге I ndex и с помощью цикла Листинг 5.21. fo r e ach Pr oduc t []. View () 5.21 141 который содержит простые для визуализации с применением изменен тип модели для представления производится проход по объектам в массиве. Проход по массиву в файле Index. cshtml из папки Views/Home @model Product[] @{ Vi ewBag. Tit l e "Product Name"; <tаЫе> <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> </tаЫе> Оператор @fo reach выполняет проход по содержимому массива объектов моде­ ли и генерирует для каждого элемента массива строку в таблице. Как видите, в цик­ ле f oreach создается локальная переменная по имени р, на свойства которой про­ изводится ссылка с использованием выражений Результат приведен на рис. D Product №mo С ,Ф Razor вида @р . Na me 5.11. х localhost 60753 P1·oduct Information Name Price Kayak f275.00 Lifejacket f48.95 Sоссег ball fl9.50 Согпег flag f34.95 Рис. 5.11. Применение Razor для прохода по массиву и @ р. Pr i c e . 142 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Резюме В главе был предложен обзор механизма визуализации Razor и показано. как его использовать для генерации НТМL-разметки. Мы выяснили, каким образом ссылать­ ся на данные, передаваемые из контроллера, через объект модели представления и объект ViewBag, а также продемонстрировали применение выражений Razor для подстройки ответов пользователю на основе значений данных. В оставшихся главах книги вы увидите много разных примеров использования Razor. а в главе 21 найдете MVC. В следую­ предлагаемые Vlsual Studio для работы подробное обсуждение функционирования механизма визуализации щей главе будут описаны некоторые средства. с проектами ASP.NET Core МVС. ГЛАВА 6 Работа с Visual Studio настоящей главе рассматриваются основные средства, предоставляемые Visual в Studio 6.1 ASP.NET Core MVC. В табл. для разработки проектов приведена свод­ ка по главе. Таблица 6. 1. Сводка по главе Листинг Задача Решение Добавление пакетов к проекту Используйте инструмент пакетов .NET и Bower для NuGet для 6.6-6.8 пакетов клиентской стороны Просмотр результатов изменения Применяйте модель итеративной представления или класса разработки Отображение подробных сообщений в браузере разработчика Получение детальной информации и Применяйте отладчик 6.9-6.11 Используйте страницы исключений 6.12 6.13 контроля над выполнением приложения Повторная загрузка одного или боль­ Используйте средство шего числа браузеров с применением (Ссылка на браузер) Browser Liпk 6.14,6.15 Visual Studio Сокращение количества НТТР-запросов Применяйте расширение Buпdler и ширины полосы пропускания, требуе­ Minifier мой для файлов JavaScript и & 6.16-6.23 (Упаковщик и минификатор) CSS Подготовка проекта для примера Для целей текущей главы создадим новый проект по имени Wо r k i n gW i t h VisualStudio типа ASP.NET Соге Web Application (Веб-приложение ASP.NET Core) из группы .NET Core, используя шаблон Empty (Пустой). Включим инфраструктуру MVC с ее стандартной конфигурацией в классе Startup (листинг 6.1). Листинг 6.1. Включение инфраструктуры из папки using using using using MVC в файле WorkingWi thVisualStudio System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Startup. cs 144 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 using using using using Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace WorkingWithVisualStudio { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); } puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseМvcWithDefaultRoute(); } Создание модели Создадим папку Models и добавим в нее файл класса по имени ределением, показанным в листинге Листинг 6.2. Содержимое файла Product. cs с оп­ 6.2. Product. cs из папки Мodels namespace WorkingWithVisualStudio.Models puЬlic class Product { string Name { get; set; } puЫic decimal Price { get; set; puЬlic Чтобы создать простое хранилище объектов файл класса по имени веденное в листинге Листинг 6.3. SimpleRepository.cs Product, добавим в папку Models и поместим в него определение, при­ 6.3. Содержимое файла SimpleReposi tory. cs из папки Мodels using System.Collections.Generic; namespace WorkingWithVisualStudio.Models puЬlic class SimpleRepository { private static SimpleRepository sharedRepository = new SimpleRepository(); private Dictionary<string, Product> products = new Dictionary<string, Product>(); puЬlic puЫic static SimpleRepository SharedRepository => sharedRepository; SimpleRepository() { var initialitems = new[] { new Product { Name "Kayak", Price = 27 SM } , new Product { Name "Lifejacket", Price = 48.95М ), new Product { Name "Soccer ball", Price 19. SOM } , new Product { Name "Corner flag", Price = 34.95М 1 1; Глава 6. Работа с Visual Studio 145 foreach (var р in initialitems) { AddProduct(p); puЫic puЬlic Класс IEnumeraЫe<Product> Products => products.Values; void AddProduct(Product р) => products.Add(p.Name, SimpleRepository хранит в модель изменения утрачиваются, р); объекты модели в памяти, т.е. любые внесенные когда приложение останавливается или переза­ пускается. Для примеров, приводимых в настоящей главе, непостоянного хранилища вполне достаточно, но такой подход не может применяться во многих реальных про­ ектах; в главе 8 рассматривается пример создания хранилища, которое запоминает объекты модели на постоянной основе, используя реляционную базу данных. На заметку! В листинге 6.3 определено статическое свойство по имени SharedReposi tory, SimpleReposi tory, который может предоставляющее доступ к единственному объекту применяться повсюду в приложении. Это не является установившейся практикой, но я хочу продемонстрировать распространенную проблему, с которой вы столкнетесь при разработке приложений MVC; в главе 18 будет описан более эффективный способ работы с разделяемыми компонентами. Создание контроллера и представления Добавим в проект папку HomeController. cs, Листинг 6.4. и поместим в нее файл класса по имени Controllers в котором определен контроллер, показанный в листинге Содержимое файла HomeController. cs из папки 6.4. Controllers using Microsoft.AspNetCore.Mvc; using WorkingWithVisualStudio.Models; namespace WorkingWithVisualStudio.Controllers puЫic class HomeController : Controller { puЫic IActionResult Index() => View(SimpleRepository.SharedRepository.Products); Здесь имеется единственный метод действия екты модели и передает их методу View () Index (),который получает все объ­ для визуализации стандартного представ­ ления. Чтобы добавить стандартное представление, создадим папку поместим в нее файл представления по имени приведено в листинге Index. csh tml, 6.5. Листинг 6.5. @model IEnumeraЬle<WorkingWithVisualStudio.Models.Product> Содержимое файла @{ Layout = null; <!DOCTYPE html> Index. cshtml из папки Views/Home и содержимое которого Views/Home 146 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 <html > <head> <meta name ="viewport" co ntent="width =device-width" /> <t itle>Working with Vi s ual Studi o</ title> </ head > <b o dy> <t аЫе> <thead > <tr> <td >Name</td ><t d >Price</td >< /tr> </thead > <tb o dy > @f o r eac h (var р i n Model ) <tr > <td >@p.Name</td > <td >@p.Price</td> </tr > </ tb ody > < /tаЫе> < /body> </html> Представление включает таблицу . в которой с помощью Rаzоr-цикла f o rea c h со­ здаются строки для всех объектов модели, причем каждая строка содержит ячейки для значений свойств Name и Price. 6. 1. После запуска примера приложения отобразятся результаты, показанные на рис. С [G -i _-l_o_c_a-lh_o_s_t_:64 - 405 _ -==--=-=-=--===:-=::::::::-===== Name Kayak Lifejacket Soccerball Comer flag Price 275 48.95 19.50 34.95 Рис. 6.1. Выполнение примера приложения Управление программными пакетами Проектам ASP.NET Core МVС требуются два разных вида программных пакетов. Такие типы пакетов будут описаны в последующих разделах вместе с инструментами. которые среда Visual Studio предлагает для управления ими. Глава 6. Работа с Visual Studio Инструмент Среда кетами NuGet Visual Studio .NET. 147 предоставляет графический инструмент для управления па­ включаемыми в проект. Чтобы открыть инструмент NuGet, выберем в Tools<:>NuGet Package Maпager (Сервисе=> Диспетчер пакетов NuGet) пункт Мапаgе NuGet Packages for Solutioп (Управление пакетами NuGet для решения) . Инструмент NuGet откроется и отобразит список пакетов, которые уже установлены (рис. 6 .2). меню lnstalled Browse Updates Consolidate Р Sear<h (Ct,f+E) "G Manage Packages for Solution О lncludt prereleilst Package sourc~ nuget.org · - · - · ~ Microso~.AspNetcore.All • Vtrsion(s) · 1 • Q v2.0.0 Microsoft . NEТCore . App Ьу м;crosoft А Proj<ct О Worl<:ingWrthVisu1tStudio s.tt of .NЕТ APl 's that ,щ~ included in tht def•ult .N ЕТ Cort •pplic"•tion modtl. е8Ь88б 1ас 71•f042c8711<2f9f 2d04c98b69f 28d lnst.tffed: 2.0.О tin r~ t.il Vcrsion: 1Uttst staЫt 2.0.О ~"~ 9 °",.,.,, Each ~ck•3t is licмsed to you Ьу its owntr. NuGtt 1s not t6pons1ble for, nor dots it gr•nt апу l1censts to, thtrd·p•rty p.1c.kJge.s. О Оо not show th1s 19lin Deкrtption Рис. 6.2. Использование диспетчера пакетов NuGet На вкладке lпstalled (Установленные) приводится сводка по пакетам. уже установ­ ленным в проекте. Вкладку Browse тановки новых пакетов, а вкладку (Обзор) можно применять для нахождения и ус ­ Updates (Обновления) - чтобы получить список пакетов, для которых были выпущены свежие версии . Пакет Мicrosoft . AspNetCore Если вы работали с более ранними версиями ASP.NET Core, мости добавления в новый проект длинного списка пакетов .All тогда вам известно о необходи­ NuGet. В версии ASP.NET Core 2 принят другой подход , который опирается на единственный пакет по имени Mi c r o s oft . Asp Net Core . Al l. Пакет Mi c r o s of t. Asp NetCo re. Al l индивидуальные пакеты турой MVC, NuGet, представляет собой мета-пакет, содержащий все которые требуются платформой ASP.NEТ Core и инфраструк­ т.е. добавлять пакеты по одному не придется . При опубликовании приложения любые пакеты, которые входят в состав мета-пакета, но в приложении не используются, будут удалены, гарантируя тем самым , что лишние пакеты развертываться не будут. 148 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Список и местоположение пакетов Инструмент NuGet NuGet отслеживает пакеты проекта в файле <имя проекта>. csproj. В текущем примере приложения это означает, что детали. касающиеся пакетов NuGet. хранятся в файле по имени не отображает файл WorkingWithVisualStudio. csproj. Среда Vlsua\ Studlo . cspro j в окне Solution Explorer. Чтобы отредактировать данный файл, щелкнем правой кнопкой мыши на имени проекта в окне Solution Explorer и Edit WorkingWithVisualStudio.csproj (Редактировать WorkingWi thVisualStudio. csproj). Среда Visua\ Studio откроет файл для редак­ тирования. Файл . csproj имеет формат ХМL и содержит элемент вроде показанного ниже. который добавляет к проекту мета-пакет ASP.NET Core: выберем в контекстном меню пункт <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" / > </ItemGroup> Пакет указывается с помощью своего имени и номера требуемой версии. Хотя мета­ пакет включает все функциональные средства. требующиеся для ASP.NET Core МVС. вам по-прежнему понадобится добавлять пакеты в проект. чтобы появилась возможность применения дополнительных средств. Пакеты можно добавлять с использованием гра­ фического интерфейса, показанного на рис. 6.2, мандной строки . Кроме того, файл можно редактировать и среда . cspr o j либо с применением инструментов ко­ Visual Studio обнаружит изменения, после чего загрузит и установит добавленные пакеты. Когда для добавления в проект какого-то пакета используется инструмент NuGet, пакет автоматически устанавливается вместе со всеми пакетами, от которых он за­ висит. Исследовать пакеты NuGet и их зависимости можно, раскрыв в окне Solution DependenciesqNuGet (ЗависимостиqNuGеt), что приведет к отображе­ нию всех пакетов в файле . csproj и их зависимостей. Мета-пакет ASP.NET Core со­ держит большое количество зависимостей, часть которых видна на рис. 6.3. Explorer узел Solution Explorer G\ а"· 1 'ф Se~r<h ~ • l\\i1 Г® ... в Solut1on Explorer (Ctrl+;) l:jj Solution 'WorkingWithVisualStudio' (1 project) ~ " q, .~-.· 1> " Connected Seivices Dependencies '(у-.: Analyzers ·~ NuGet " ·~ Microsoft.AspNetCore.All (2.0.0) 1> ·~ Microsoft.AspNetCore (2.0.0) 1> ·~ Microsoft.AspNetCore.Antiforgery (2.0.0) 1> ·~ Microsoft.AspNetCore.Applicationlnsights.HostingStartup (2.0.0) ~ ·~ Microsoft.AspNetCore.Authentication (2.0.0) 1> ·~ Microsoft.AspNetCore.Authentication.Abstractions (2.0.0) 1> ·~ Microsoft.AspNetCore.Authentication.Cookies (2.0.0) 1> ·~ Microsoft.AspNetCore.Authentication.Core (2.0.0) 1> ·~ Microsoft.AspNetCore.Authentication.Face ook (2.0.0) •о Рис. 6.3. Узел ~pNe f•ще ""tjr. DependenciesqNuGet ·~. в окне Solution Explorer Глава 6. Работа с Visual Studio Инструмент 149 Bower Пакет клиентской стороны включает содержимое , отправляемое кл иенту, в чис­ ле которого файлы таблицы стилей JavaScript. CSS либо изображения . Для управле ­ NuGet, но инфраструктура ASP.NET Соге MVC теперь полагается на инструмент под названием Bower. Инструмент с открытым кодом Bower был разработан за пределами Microsoft и мира .NET и широ­ ко применялся при разработке неб-приложений, отличных от ASP.NET. ния пакетами подобного рода также можно задействовать На заметку! Недавно инструмент Bower был объявлен устаревшим. Вы можете видеть пре­ Bower все Visual Studio. Можно дупреждения, рекомендующие альтернативные инструменты; тем не менее , еще активно сопровождается, а поддержка ожидать, что в какой-то момент в Microsoft Bower интегрирована в решат поддерживать другой инструмент для управления пакетами клиентской стороны, но пока этого не случилось, вы должны про­ должить пользоваться Список пакетов Bower. Bower b ower. j s o n. Чтобы создать файл bower. j s o n , Wo r kingWithVisua lStudio в окне Solutioп Explorer. выберем в контекстном меню пункт AddqNew ltem (ДобавитьQНовый элемент) и укажем шаблон элемента Bower Coпfiguratioп File (Файл конфигурации Bower) из категории ASP.NET Core QWebQGeпeral (ASP.NET СоrеQВебQОбщие) , как по ­ казано на рис . 6.4. Пакеты Bower указываются в файле щелкнем правой кнопкой мыши на имени проекта " lnstalled Core Code General Web ASP.NET General Scripts Content А ASP.NEТ А ~ ASP.NEТ Configuration File npm Configuration File ASP.NET Core Gulp Configuration File ASP.NEТ rJS rJS Online g 6J J) ASP.NET Core Grunt Configuration File ASP.NET Core JSON File ASP.NEТ Соге JSON Schema File ASP.NEТ Core XML File ASP.NEТ Name: bower.json Рис. 6.4. Core Создание файла конфигурации Bower Core Ту 150 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Среда ке Visual Studio установит bower. j son в кач естве имени и после щелчка на кноп­ (Добавить) добавит в проект файл со стандартным содержимым (листинг Add Листинг 6.6. Стандартное содержимое файла bower. j son 6.6). из папки WorkingWithVisualStudio " na me " : " asp . net ", " private ": t r ue , " dependencies ": В ли стинг е bower. j son . демонстрируется добавление п а к ета кли е нтской стороны в файл е 6. 7 что делается включением записи в р аздел Совет. По адресу ht t p : //bower.io/search dependencies. находится хранилище пакетов Bower, где можно искать пакеты для добавления в свои проекты. Листинг 6. 7. Добавление пакетов в файле bower. j son из папки WorkingWithVisualStudio " na me " : " as p .net " private ": tr ue , "dependencies ": { 11 , "bootstrap": "3. 3. 7" Выдел е нная полужирным строка обеспечивает доб авление в пример проект а СSS­ пакета Bootstrap. При редактировании файла bower. j son среда Vis ual Studio 6 .5). пр едло­ жит с писок имен пакетов и перечислит доступные версии пакетов (ри с . ~ \'lorking\AfrthV1sualStudio - r:I Schema: http://json.schemastore.org/ bower l 8 { 2 1 "name" : "" a sp.n~t" J "private" : true, 3 4 "dependencies" : { s 6 7 8 100% "Ьootstrap" : t } '} !!! ~ The latffi staЫe version of the package т ЛЗ.3.7 т -3.3.7 • Рис. 6.5. Перечисление доступных версий пакетов клиентской стороны Х . + Глава 6. Работа с Visual Studio 151 bootstrap была 3.3.7. Visual Studio предлагает три варианта: 3 . 3 . 7, л 3 . 3 . 7 и -3. 3. 7. Номера версий в файле bower. j son могут указываться в виде диапазо­ на разнообразными способами, наиболее полезные из которых описаны в табл. 6.2. На момент написания главы последней версией пакета Однако обратите внимание, что Самый безопасный способ указания пакета предусматривает применение явного но­ мера версии. Это гарантирует, что вы всегда будете работать с определенной версией до тех пор, пока преднамеренно не обновите файл bower. j son для запроса другой версии. Таблица 6.2. Распространенные форматы для номеров версий в файле Ьower. j son Формат 3.3.7 Описание Выражение номера версии напрямую приведет к установке пакета с точ­ но совпадающим номером версии, например, * 3.3.7 Использование символа звездочки позволяет инструменту Bower загру­ жать и устанавливать любую версию пакета >3.3.7 >=3.3.7 > или >= позволяет инструменту версию пакета, которая больше либо боль­ Снабжение номера версии префиксом Bower устанавливать любую ше или равна заданной версии <3.3.7 <=3.3.7 < или <= позволяет инструменту версию пакета, которая меньше либо мень­ Снабжение номера версии префиксом Bower устанавливать любую ше или равна заданной версии -3.3.7 Снабжение номера версии префиксом в виде символа трументу Bower устанавливать - позволяет инс­ версии, даже если номер уровня исправ­ лений (последнее число в номере версии) не совпадает. Например, ука­ -3. З. 7 позволяет инструменту Bower устанавливать версию 3.3.8 3.3.9 (которая будет исправлена до версии 3.3.7), но не версию 3.4.0 зание или (которая будет новым младшим выпуском) лз.З.7 Снабжение номера версии префиксом в виде символа л позволяет инс­ трументу Bower устанавливать версии, даже если номер младшего вы­ пуска (второе число в номере версии) или номер исправления не совпа­ дает. Например, указание л3. 3.3.1, 3.4.0 и 3.5.0, 3. О позволит Bower устанавливать 4.0.0 версии но не версию Совет. Для примеров в настоящей книге файл bower. j son создавался и редактировал­ ся напрямую. Редактировать файл очень просто, к тому же это способствует получению предсказуемых результатов при проработке примеров. Среда ставляет графический инструмент для управления пакетами крыть, щелкнув правой кнопкой мыши на имени файла тном меню пункт Мапаgе Bower Packages Visual Studio также предо­ Bower, который можно от­ bower. j son (Управление пакетами и выбрав в контекс­ Bower). Visual Studio отслеживает изменения в файле bower. j son и автоматически Bower для загрузки и установки пакетов. После сохранения изменений, внесенных в файл согласно листингу 6.7, среда Visual Studio загрузит па­ кет Bootstrap и установит его в папку wwwroot/lib (рис. 6.6). Среда задействует инструмент 152 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 ... Solution Explorer ·l fэ·!;@f® I ~-- &.) Search Solution Explorer (Ctrl+ ;) ~~·~~~~~~ ~ NuGet Р· lib Рис. Подобно ~ х ~ bootstrap ~ jquery Добавление в проект пакетов клиентской стороны 6.6. инструмент Bower управляет зависимостями пакетов, добавляемых в проект. Некоторые расширенные функциональные средства пакета сят от JavaScript-библиoтeкиjQuery. из-за чего на рис . 6.6 Bootstrap зави­ видны два пакета. Для про­ смотра списка пакетов и их зависимостей необходимо развернуть узел Depeпdencies (Зависимости) в окне Solution Explorer (рис. 6. 7). Solution Explore.r "' t:I Х · 1 0·~Sil'i№ ! J--- 111- Se3rch Solution Explorer (Ctrl•;) .~i· ~ Dependencies Anal)ILers •.,, NuGet ~ ~ SDK ~ 'fi~ " Рис. 6. 7. 1·1 bootstrap (3.З .7) •·• jquery (З.2.1) Исследование пакетов клиентской стороны и их зависимостей Обновление пакета Bootstrap В оставшемся материале книге применяется предварительный выпуск инф­ раструктуры Bootstrap Bootstrap CSS. Во время написания книги команда разработчиков занималась построением версии Bootstrap 4 выпусков. Выпуски были помечены словом alpha. и произвела несколько ранних но обладали высоким качеством. достаточным для их использования в примерах данной книги. Находясь перед вы­ бором, применять версию Bootstrap 3. 4, я предварительный выпуск Bootstгap которая скоро выйдет из употребления, или решил использовать новую версию, несмотря на то, что имена классов. применяемых для стилизации элементов HTML. в финаль­ ном выпуске вполне могут измениться. Это означает, что для получения ожидаемых результатов от примеров вы должны использовать ту же самую версию Чтобы обновить пакет тинг 6.8). Bootstrap. изменим номер версии в файле Bootstrap. bower. j son (лис­ Глава 6. Работа с Visual Studio Листинг 6.8. Изменение версии пакета из папки Bootstrap в файле 153 bower. j son WorkingWi thVisualStudio "name": "asp. net", "private": true, "dependencies": ( "bootstrap": "4. О. 0-alpha. 6 11 После сохранения изменений, внесенных в файл загрузит новую версию пакета bower. j son, среда Visual Studio Bootstrap. Итеративная разработка Разработка веб-приложений часто может быть итеративным процессом, когда вы вносите небольшие изменения в представления или классы и запускаете приложение, чтобы протестировать результат. Инфраструктура МVС и среда Visual Studio на пару поддерживают такой итеративный подход. делая наблюдение за влиянием изменений быстрым и легким. Внесение изменений в представления Razor Во время разработки изменения, внесенные в представления Razor. вступают в силу, как только поступает НТТР-запрос от браузера. Чтобы продемонстрировать, каким образом это работает, запустим приложение, выбрав пункт (Запустить отладку) в меню Debug отображающей данные, внесем в файл листинге Листинг Start Debugging (Отладка), и после открытия вкладки браузера. Index. cshtml изменения, приведенные в 6.9. 6.9. Внесение изменений в файле Index. cshtml из папки Views/Home @model IEnumeraЬle<WorkingWithVisualStudio.Models.Product> @( Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Working with Visual Studio</title> </head> <body> <hЗ>Products</hЗ> <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> 154 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 </tbody> < /tаЫе> </body> </h tml> Сохраним изменения в представлении I ndex и перезагрузим текущую веб-стра­ ницу с помощью кнопки обновления в браузере. Изменения в представлении (добав­ ление заголовка и форматирование свойства Price объекта модели как денежного значения) оказывают воздействие и отображаются в браузере (рис. D Worl:ing with VRu•I Sl\J. С 6.8). )( Lф localhost:52589 P1·odt1cts Naine Price Кауаk 5275.00 Lifejacker 548.95 Socce1· ball 519.50 Соrне1· flag 534.95 Рис. 6.8. Результаты внесения изменений в представление Совет. Процесс подготовки к использованию представлений Razor рассматривается в главе 21 . Внесение изменений в классы С# Для классов С# , включая контроллеры и модели, способ обработки изменений за­ висит от того. как запускается приложение. В последующих разделах будут показаны два доступных подхода, которые инициируются посредством разных пунктов в меню Debug и кратко описаны в табл. Таблица б.З. Пункты меню Пункт меню 6.3. Debug Описание Start Without Debugging Классы в проекте компилируются автоматически, когда посту­ (Запустить без отладки) пает НТТР-запрос, делая возможной более динамичную прак­ тику разработки. Приложение запускается без отладчика, что не позволяет взять под контроль выполнение кода Start Debugging При таком стиле разработки потребуется явно компилиро­ (Запустить отладку) вать проект и перезапускать приложение, чтобы изменения вступили в силу. Во время выполнения к приложению присо­ единен отладчик, позволяя инспектировать его состояние и анализировать причины возникновения любых проблем Глава 6. Работа с Visual Studio 155 Автоматическая компиляция классов В течение нормальной стадии разработки быстрый итеративный цикл позволяет видеть результаты изменений немедленно независимо от того, связаны они с добав­ лением нового действия или с изменением способа. которым выбираются данные мо­ дели представления. Для разработки такого вида среда Visual Studio поддерживает обнаружение изменений. как только от браузера получен НТТР-запрос, и автомати­ ческую перекомпиляцию классов. Чтобы посмотреть, как это работает. выберем пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual Studio. После отображения данных приложения в браузере внесем в контроллер Home изменения. показанные в листинге 6.1 О. Листинг 6.10. Фильтрация данных модели в файле HomeController . cs из папки Views/Home u si ng Mi c roso ft.AspNet Co re.Mvc ; u si ng Wo rkingWithVisualStudio.Mo dels; using System.Linq; namespace WorkingWithVisualStudi o .Controllers puЫic class Ho meController : Con troller { p u Ыi c I Ac ti o nResult Index ( ) => View(SimpleRepository.Shared.Repository.Products .Where(p => p.Price < 50)); Изменения заключаются в применении LINQ для фильтрации объектов чтобы представлению передавались только те из них , свойство значение меньше 50. Сохраним Pri ce Prod uct, которых имеет изменения. произведенные в файле класса контрол ­ лера, и обновим окно браузера. не останавливая или не перезапуская приложение в Visual Studio. Отправленный браузером НТТР-запрос инициирует процесс компиля­ ции. и приложение будет запущено повторно с использованием модифицированного класса контроллера. генерируя результаты, в которых из таблицы исчез товар (рис. 6.9). D Working v1ith Visшl S С Ф localhost52589 P1·od11cts Price №ше Lifejacket $48.95 Soccer ball $ 19. 50 Соше1· flag $34.95 Рис. 6.9. Автоматическая компиляция классов Ka yak 156 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Средство автоматической компиляции удобно, когда все идет по плану. Его недо­ статок в том, что ошибки этапа компиляции и времени выполнения отображаются в браузере, а не в Visual Studio, затрудняя выяснение причины возникновения пробле­ мы. В качестве примера в листинге 6.11 демонстрируется добавление ссьmки null в коллекцию объектов моделей внутри хранилища. Листинг 6.11. Добавление ссыпки из папки null в файле 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Ыic static SimpleRepository SharedRepository => sharedRepository; SimpleRepository() { var initialitems = new[] { "Kayak", Price = 27 5М } , new Product { Name "Lifejacket", Price = 48.95М ), new Product { Name 19. 50М } , "Soccer ball", Price new Product { Name "Corner flag", Price = 34.95М) new Product { Name puЬlic }; foreach (var р in initialitems) { AddProduct(p); products.Add("Error", null); puЬlic IEnumeraЬle<Product> puЫic Products => products.Values; void AddProduct(Product Проблема вроде ссылки null р) => products.Add(p.Name, р); не будет видна до тех пор, пока приложение не на­ чнет выполняться. Перезагрузка страницы в браузере приведет к тому, что класс скомпилируется, а приложение запустится заново. Когда ин­ SimpleRepository фраструктура MVC создает экземпляр класса контроллера для обработки НТТР­ запроса от браузера, конструктор SimpleRepository, HomeController создаст экземпляр класса который в свою очередь попытается обработать ссьmку добавленную в листинге 6.11. Ссьmка null null, вызовет проблему, но ее сущность не бу­ дет очевидной, потому что браузер не выводит сколько-нибудь полезное сообщение. Включение страниц исключений разработчика Во время процесса разработки при возникновении проблемы удобно отображать в окне браузера более полезную информацию. Это можно делать, разрешив страницы исключений разработчика, что требует изменения конфигурации в классе как показано в листинге 6.12. Startup, Глава 6. Работа с Visual Studio Роль класса Startup 157 подробно объясняется в главе что вызов расширяющего метода 14, а пока достаточно знать, UseDeveloperExceptionPage () устанавливает описательные страницы ошибок. Листинг 6.12. Разрешение страниц исключений разработчика в файле startup. cs из папки using using using using using using using using WorkingWi thVisualStudio System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; namespace WorkingWithVisualStudio { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services ) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseMvcWithDefaultRoute(); Если перезагрузить окно браузера, то процесс автоматической компиляции пере­ строит приложение и выдаст более полезное сообщение об ошибке (рис. С! lntornol Sorvor Error С !Ф 6.1 О). х localhost:64405 An unhandled exception occurred while processing the request. NullReferenceExcep(ion: Objec reference no set to an instance of ап object. WorkingW1thVisualStudio.Controllers.HomeController+ <>C.<lndex> b_O_O(Product pJ in OL1ery Cookies Кomecontroll•r. cs, line Headers NullReferenceExcep iоп: Object reference not set to an instance of ап object. \'>'o":ir'g~Vit Visua 5 udю.Cortrcl:ers l"'OmeCo" tro er-<>C.<I dex>b_O_O(P rodl.д о) HomeController . cs Рис. 6.1 О. Страница исключения разработчика 1n 10 158 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Отображаемого в браузере сообщения об ошибке может быть достаточно для вы­ явления простых проблем, особенно из-за того, что итеративный стиль разработки означает, что причиной. вероятно, стали самые последние изменения. Но для устра­ нения более сложных проблем - медленно - требуется отладчик и проблем. которые не становятся очевидными не­ Visual Studio. Использование отладчика Среда Visual Studio также поддерживает выполнение приложения MVC с примене­ нием отладчика, который позволяет останавливать приложение для инспектирования его состояния и пути прохождения запроса по коду. Это требует другого стиля раз­ работки, поскольку модификации классов С# не будут применены до тех пор, пока приложение не запустится заново (хотя изменения, вносимые в представления Razor, по-прежнему вступают в силу автоматически). Такой стиль разработки не настолько динамичен. как использование средства автоматической компиляции, но отладчик Visual Studio великолепен и может предо­ ставлять намного более глубокое проникновение в суть способа работы приложения. нежели то, что возможно с помощью сообщений, отображаемых в окне браузера. Чтобы запустить приложение с применением отладчика, выберем пункт Start Debug (Отладка) среды Visual Studio. Перед за­ пуском приложения среда Visual Studio скомпилирует классы С# в проекте. но код можно также скомпилировать вручную, используя пункты меню Build (Построение). Debugging (Запустить отладку) в меню Пример приложения по-прежнему содержит ссылку null, т.е. необработан­ NullReferenceExcepti o n. которое генерируется в классе SimpleRepo si tory, прервет приложение и передаст управление выполнением от­ ное исключение ладчику (рис. 6. l l ). . 1 ;\ using Microsoft.AspНetCore .Мvс; using WOrkingl-lithVisu•lStudio.llodels; using System. l inq; 8 namespace WOrki ngWi thVisualStudio.Cont rollers { В puЫic class Нol\"e{ontroller : Controller { 1 т Q 10 11 12 l puЬlic !ActionResult Index(} •> View( Si111pleReposi tory . ShllredReposi tory. Pгoduc ts .Where( p •> p.Price < 50}); О l} Exception Thrown 13 System.NullReferenceException: 'OЬject reference not set to "" 1nst1nce of an object.' View А 100,.-. !Жa•ls Сору Deta~s Exception Settings 0 8re6k \Yhen th1s except1on type is thrown Except \•"hen thro\•m from: О 'NorkingWithVisualStudio.dll ... Рис. 6. 11. Возникновение необработанного исключения q х Глава 6. Работа с Visual Studio 159 Совет. Если отладчик не перехватывает исключение, тогда понадобится выбрать пункт Settings (ОкнаqНастройки исключений) в меню Debug (Отладка) Visual Studio и удостовериться, что в списке Commoп Laпguage Runtime Exceptions (Общие исключения языка во время выполнения) отмечены флажки для всех WiпdowsqException среды типов исключений. Настройка точки останова Отладчик вовсе не показывает основную причину проблемы. а только место, где она проявилась. Подсвечиваемый Visual Studio оператор указывает на то, что про­ блема случилась при фильтрации объектов с применением LINQ, но для получения деталей и выявления лежащей в основе причины потребуется проделать небольшую работу. Точка останова - это инструкция, которая сообщает отладчику о необходимости остановить выполнение приложения и передать управление программисту. Вы може­ те исследовать состояние приложения, посмотреть, что произошло, и при желании возобновить выполнение. Чтобы создать точку останова, щелкнем правой кнопкой мыши на операто­ ре кода и выберем в контекстном меню пункт Breakpointq lnsert Breakpoint (Точка остановаqВставить точку останова). В целях демонстрации поместим точку останова на метод AddProduct () в классе new Product { Name • 14 1 15 16 17 18 19 l ~ 2е • 6.12). Price • 27SM } , new Product { ~lame • "Lifejac ket", Price • 48.951-' }, new Pt"Oduct { rll!1me • "Sоссег b4ll". Price • 19. sем } , new Product { f4ам • "Corner flag"' , Price • 34.9SH } }; foreach (var р in initialtt~s) { AddProduct(p); puЫic Ifnшreгt1Ьle<Pгoduct > 27 Products •> products..Valuesi puЫic void AddProd~t( Pгoduct р) •> Ml§ldft@lф.!M!)J; • 28 29 100 % " Каувk" , (рис. products .Add( "Error" , null); 21 22 23 24 25 2б Simp l eRepo si t o r y l} Рис. Выберем пункт меню 6.12. Создание точки останова Debugq Start Debugging (Отладкаq Запустить отладку), чтобы запустить приложение, используя отладчик. или Debugq Restart (Отладкаq Перезапустить), если приложение уже выполняется . Во время обработки начального НТТР- запроса от браузера будет создан экземпляр класса Simpl e Reposi to ry, а вы ­ полнение кода достигнет точки останова, где произойдет пауза. Во время паузы можно применять пункты меню Debug среды Vlsual Studio или эле­ менты управления в верхней части окна для управления выполнением приложения либо использовать разнообразные представления отладчика, доступные через меню Debug~Windows (ОтладкаqОкна). чтобы инспектировать состояние приложения. 160 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Просмотр значений данных в редакторе кода Наиболее распространенное применение точек останова связано с отслеживанием дефектов в коде. Прежде чем можно будет исправить дефект, потребуется выяснить. что происходит. и одним из самых полезных средств, предлагаемых Visual Studio. яв­ ляется возможность просмотра и наблюдения за значениями переменных прямо в ре­ дакторе кода. Если навести курсор мыши на аргумент р в методе AddProduct () . под­ свеченном отладчиком. то появится всплывающее окно. которое показывает текущее значение р (рис. поэтому на рис. ' ·~ tf 1'• 6.13). Рассмотреть всплывающее окно может 6.13 показана также его увеличенная версия. ! ··~'·,~ .... 1~ Workin9WrthVisu1IStud~Мodels.SimpltRep · f19 Stmpltkpository() ntw Product { filee • "1(1yak" , Price • 27SМ }, ntw Product { tlame • "lifejacktt". Price • 48 . 9SМ }, nht ProduGt { па111е • "Soccer ball w, Price • 19. sен }, 3 14 " tj - - ~07 m~Vasu11Studio 10 17 18 19 быть затруднительно. х [+ nnr1 Product { Nal\f: • "Corner fla1" , Price • '4 . 9S.Н } }; ' foreech (var F. р in inir--.,....,-..,-----..,--..,------":----,,, • ~ { 1'Jolicin WithVisu11IStudio.Models.Product} Q" AddProduct(p!; 2е products . Add( "Erгcr", 21 )' №me )' Price 22 23 24 puЫic IEn.neraЫe < P"'Oduct > ,. Products •> products.V111lues; 2S Q 27 28 29 100% .. Рис. 6.1 З. Инспектирование значения данных Результат не кажется особо впечатляющим, т.к. объект данных определен в том же конструкторе. что и точка останова, но данное средство работает для любой перемен­ ной. Можно исследовать переменные, чтобы увидеть значения их свойств и полей. Правее каждого значения находится небольшая кнопка с изображением канцелярс­ кой кнопки, которую можно использовать для наблюдения за значением переменной. когда выполнение кода продолжится. Наведем курсор мыши на переменную р и щел­ кнем на кнопке с изображением канцелярской кнопки, чтобы закрепить ссылку на Product. Развернем закрепленную ссылку, чтобы также закрепить свойства Name Price, получив в итоге результат, который представлен на рис. 6.14 . . ull); t"roducts •> products. Valu~s; •uct р) •> products.Add(p . 11....,, • р); -р /'p.Name "p.Pnet {Worlcingl'frthVisualStudio Modtls.Product) р - 'Коу> k" 275 . Рис. б. 14. Закрепление значений в редакторе кода и Глава 6. Работа с Visual Studio Выберем пункт Continue (Продолжить) в меню Debug (Отладка) среды 161 Visual Studio. чтобы продолжить выполнение приложения. Поскольку приложение выполняет цикл foreach. когда точка останова встретится опять , то снова возникнет пауза . Закрепленные значения показывают, как изменяется объект. присвоенный перемен­ ной р, и его свойства (рис. 6.15). ' 8QI р {WorkingWithVisualStudio.Мodels.Product} JJ p.Nc1me Р •ufejacket' iJ p.Prke --48-.9-S_ _ _ _ _ _ _ _ _ _ Рис. 6.15. J Наблюдение за изменением состояния с применением закрепленных значений Использование окна Locals Связанным средством является окно тем выбора пункта меню Locals отображаются Locals (Локальные), которое открывается пу­ Debugc::>Windowsc::>Locals (Оrладкас::>Окнас::>Локальные). В окне значения данных похожим на закрепленные значения образом, но здесь присутствуют все локальные объекты. относящиеся к точке останова (рис . loc•ls Name Value Туре Count = 2 System.C ollections.Goneric .IEnumerable<Workir System.Collections.Generic.Di<tionary< string, W ."· ,. • [oil • )' Products • ~ products • ~ St•tic members , ~ р Count: 2 {WorkingWithVisualStudio.Models.Product) ' Soccer boll 19.SO )' Name )' Price Рис. 6.16. Каждый раз. когда выбирается пункт новляется и в цикле 6. 16). for e ach \'Joricingl'frthVisu•IStudio.Models.Product • string decimal Окно Locals Continue. выполнение приложения возоб­ обрабатывается новый объект. Продолжая поступать так, можно будет увидеть ссылку nu l l в окне Locals и в закрепленных зн ачениях , отображаемых внутри редактора кода. За счет применения отладчика для управления выполнением приложения появляется возможность отслеживать его продвижение по коду и получить представление о том, что происходит. Устранить проблему со ссылкой объектов Produc t, nu1 1 можно было бы путем очистки коллекции но альтернативный подход предусматривает повышение надеж­ ности контроллера. как показано в листинге ч ений null 6.13. где для проверки на предм ет зна­ используется null -условная операция (описанная в глав е Листинг 6.13. Исправление проблемы со ссылкой из папки Views/Home u s ing Mi c rosoft .As pNet Co re.M vc ; u si n g Wo rk i n g WithV i s u a l St ud i o .Models ; u s in g System .Li n q ; 4). null в файле HomeController. cs 162 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 namespace WorkingWithVisualS t udi o .Contr o lle r s puЫic class HomeController : Controller { IActionResult Index () => View(SimpleRepository.S har edReposit ory . r roducts . Where (р => р?. Price < 50)) ; puЫic Для отключения точки останова необходимо щелкнуть правой кнопкой мыши на операторе кода. где она находится. BreakpointqDelete Breakpoint (Точка и выбрать в контекстном меню ного запуска приложения появится простая таблица с данными (рис. D Working \~ith Visual Stu С пункт остановаqУдалить точку останова) . После повтор­ 6.17). х <D localhost;64405 P1·oducts Name Price Lifejacket f48.95 Soccer bal\ f19.50 Corner flag f34.95 Рис. 6.17. Устранение дефекта Рассмотренная проблема решалась просто. если сравнить ее с проблемами. кото­ рые требуют реальной охоты за ошибками, но отладчик Visual Studio действительно великолепен, и с помощью многочисленных доступных представлений приложения и управления выполнением можно легко углубляться в детали с целью выяснения. что пошло не так. Использование средства Средство Browser Link Browser Link (Ссылка на браузер) позволяет упростить процесс разра ­ ботки за счет помещения одного или большего числа браузеров под контроль Studio. Visual Оно особенно полезно, если нужно видеть влияние изменений в некотором диапазоне браузеров . Средство Browser Link работает с отладчиком или без него. но оно наиболее полезно в случае применения средства автоматической компиляции классов, потому что появляется воз можность модифицировать любой ф айл в проекте и видеть влияние изменения . не переходя в окно браузера и не перезагружая страни ­ цу вручную. Настройка средства Browser Link Включение средства S tartup (листинг 6.14). Browser Link требует изменения конфигурации в классе Глава 6. Работа с Visual Studio Листинг 6. 14. Browser Liпk в файле Startup. cs WorkingWithVisualStudio Включение средства из папки using using using using using using using using 163 System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace WorkingWithVisualStudio { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЬlic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseMvcWithDefaultRoute(); puЫic Использование средства арр, IHostingEnvironment env) { Browser Link Чтобы выяснить, как работает средство Browser Link, выберем пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка) среды Visual Studio. Среда Visual Studio запустит приложение и откроет новую вкладку в окне браузера для отоб­ ражения результатов. Просмотрев НТМL-разметку, отправленную браузеру. можно за­ метить, что она содержит дополнительный раздел, подобный приведенному ниже: <!DOCTYPE html> <html> <head> <meta name="viewport" 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> <tr><td>Lifejacket</td><td>&#xA3;48.95</td></tr> <tr><td>Soccer ball</td><td>&#xA3;19.50</td></tr> <tr><td>Corner flag</td><td>&#xA3;34.95</td></tr> </tbody> </tаЫе> < ! -- Visual Studio Browser Link --> <script type="application/json" 164 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 id=" browserLink initializationData"> - - {"requestld":"968949d8affc47c4a9c6326de21dfa03","requestмappingFro mServer":false} </script> <script type="text/javascript" src="http://localhost:55356/dla038413c804e178ef009a3be07b262/ browserLink" async="async"> </script> <!-- Конец Browser Link --> </body> </html> Совет. Если дополнительный раздел отсутствует, тогда нужно выбрать пункт ЕnаЫе Link (Включить Browser Link) в меню , показанном на рис. 6.18, Browser и перезагрузить страницу в браузере. Среда ментов Visual Studio script, добавляет в НТМL-разметку. посылаемую браузеру. пару эле­ которые применяются для открытия долговечного НТТР- подключения к серверу приложений, чтобы среда Visual Studio могла обеспечить принудитель­ ную перезагрузку страницы браузером. (Если вы не видите элементы удостоверьтес ь в том. что отмечен пункт ЕпаЫе рис. 6 . 18.) В листинге 6.15 Browser Link приведено изменение. внесенное в представление которое проиллюстрирует результат использования средства Bгowser Toofs Test script. тогда в меню. показанном на CodeMaid Window Index. Link. Help ; Refresh Linked Browsers Browser Linlc Oashboard Ctrl+Alt+ Enter ~ ЕnаЫе Browser Link Рис. 6.18. Применение средства Листинг 6.15. Добавление временной отметки в файле из nаnки Browser Link для перезагрузки страницы в браузере Index. cshtml Views/Home @mo del IEnumera Ыe< W or kingWith Visua lStudio.M ode l s.P rodu c t > @{ Layout = null; } < 1 DOCTYPE htm l> <html > <t1ea d> <meta n a me = " viewpor t " co nten t= "width=device-widt h" /> Глава 6. Работа с Visual Studio 165 <title>Working with Visual Studio</title> </head> <body> <h3>Products</h3> <p>Request Time: @DateTime .Now. ToString ("HH:mm: ss") </р> <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> Сохраним изменение в файле представления и выберем пункт Browsers Refresh Linked Browser Link внутри пане­ средство Browser Link не работа­ (Обновить связанные браузеры) в меню средства ли инструментов Visual Studio (см. рис. 6.18). (Если ет, тогда можно попробовать перезагрузить страницу в браузере или перезапустить Visual Studio и повторить попытку.) Код JavaScript, встроенный в отправляемую браузеру НТМL-разметку, будет пере­ загружать страницу. демонстрируя результат добавления, которым является появле­ ние простой временной отметки. Каждый раз, когда выбирается пункт меню Linked Browsers, Refresh браузер будет делать новый запрос к серверу. Результатом запроса окажется визуализация представления Index и генерирование новой НТМL-страницы с обновленной временной отметкой. На заметку! Элементы script, относящиеся к средству Browser Link, встраиваются толь­ ко в успешные ответы. Это значит, что если во время компиляции класса, визуализации представления ду браузером и Razor или обработки запроса возникает исключение, то подключение меж­ Visual Studio утрачивается и страницу придется перезагружать, используя сам брауэер, пока проблема не будет устранена. Использование нескольких браузеров Средство Вгоwsег Link может применяться для отображения приложения в не­ скольких браузерах одновременно. что удобно, когда нужно уладить вопросы несходс­ тва реализаций между браузерами (особенно при реализации специальных таблиц стилей CSS) или увидеть, как приложение визуализируется набором настольных и мобильных браузеров. Чтобы отобрать браузеры, которые будут использоваться, выберем пункт With (Просмотреть с помощью) в меню. связанном с кнопкой трументов Visual Studlo (рис. 6. 19). llS Express Browse в панели инс­ 166 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Browsers (select one or more): Firefox Add." ~leChrome &ldjrlf,f,,Ijff,gJЩiat1fm" P m lnternal Web Browser lntern~t Explorer ', Microsoft td е ' Opera lnternet Browser , Set as Default Progr~m: Size of browser window: Default Browse Рис. Среда Visual Studio 6. 19. Cancel Выбор множества браузеров отобразит список известных ей браузеров. На рис. 6.20 пока ­ заны браузеры, установленные в моей системе; часть из них установлена вместе с Windows (Internet Explorer и Edge). а часть - лично мною по причине их широкого распространения. Tools Test CodeMaid Window Help llS Express llS Express WorkingWithVisualStudio Рис. Среда 6.20. Отбор браузеров из списка Visual Studio ищет общие браузеры во Add (Добавить) можно указывать мощью кнопки время процесса установки . но с по­ браузеры. которые не были обнару­ жены автоматически . Кроме того. можно настраивать инструменты тестирования от независимых поставщиков наподобие Browser Stack. которые запускают браузеры на расположенных в облаке виртуальных машинах. так что во время тестирования не придется управлять крупной матрицей операционных систем и браузе ров. Глава 6. Работа с Visual Studio 167 На рис 6.20 выбраны три браузера: Chrome, lnternet Explorer и Edge. Щелчок на Browse (Обзор) приводит к запуску всех трех браузеров и загрузки в них URL примера приложения (рис . 6.21 ). кнопке ~ Woticing with VisU41 Stu + Е! Worki119 with Visual Stu Х Х с -------~~ P1·oducts Roquest Time: 13:18'33 Request Тime : 6.21. С) 1 localhost6ФIOS Request Time: 13:18:34 1З : 18: 34 Name Price Lifejacket f:48.95 Soccer ball f:19.50 Corner ftag f:З4.95 Рис. ~ х Products Products Name Price Lifejacket ЫS .95 Soccer ball !19.50 Com<r flag !34.95 f- о Name Pr1ce Lifejacket Е48 .95 Soccer Ьall El 9.50 Comer flag 04.95 Работа с несколькими браузерами Чтобы посмотреть, какими браузерами управляет средство Browser Link, выберем Browser Link Dashboard (Инструментальная панель Browser Link) в меню средс­ тва Browser Llnk; откроется 01шо Browser Link Dashboard (Инструментальная панель Browser Link). представленное на рис. 6.22. Инструментальная панель показывает URL. отображаемый каждым браузером. и позволяет обновлять страницу в каждом пункт браузере по отдельности. • 1:1 Browser link Dashboard .А х Workin9WithVisu11IStudio (3 connections) .А Connections Chrome.,.. Microsoft Edge .,.. Mozilla"' -1 -1 -1 learn more about Browser link Рис. 6.22. Окно Browser Link Dashboard Подготовка файлов JavaScript и CSS для развертывания При создании клиентской части веб-приложения обычно будет создаваться ряд специальных файлов JavaScript и CSS, которые дополняют аналогичные файлы в па­ Bower. Такие файлы требуют специальной обра­ кетах, установленных инструментом ботки для оптимизации их доставки в производственную среду. Цель в том, чтобы ми- 168 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 нимизировать количество НТГР-запросов и долю полосы пропускания, необходимую для доставки файлов клиенту. Процесс известен как пакетирование и минификаr~ия. В этом разделе объясняется, как включить доставку статического содержимого и ка­ ким образом подготовить такое содержимое для развертывания. Включение доставки статического содержимого Инфраструктура файлов из папки лона Empty, ASP.NET Core wwwroot располагает поддержкой для доставки статических клиентам, но когда проект создается с применением шаб­ упомянутая поддержка по умолчанию отключена. Чтобы включить под­ держку доставки статического содержимого, в класс один оператор (листинг Листинг us1ng using using using using using using using Startup необходимо добавить 6. 16). 6.16. Включение поддержки доставки статического содержимого в файле Startup.cs из папки WorkingWithVisualStudio System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace WorkingWithVisualStudio { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Добавление в проект статического содержимого Чтобы продемонстрировать процесс пакетирования и минификации. понадо­ бится добавить в проект какое-нибудь статическое содержимое и внедрить его в пример приложения. Для начала создадим папку шению хранятся специальные файлы wwwroot/css, в которой по согла­ CSS. Затем добавим файл по имени first. css, используя шаблон элемента Style Sheet (Таблица стилей), как показано на рис. 6.23. Шаблон Style Sheet находится в разделе ASP.NET CoreqWebqContent (ASP.NET СоrеqВебqСодержимое). Глава 6 Работа с Visual Studio " ASP.NET Core Code G•neral " Web Г'ti HTML Page ASP .NЕТ Core ~ Style Sheet ASP.NEТ Core LESS Style Sheet ASP.NП Core SCSS Style Sheet (SASS) ASP.NEТ Core c•e)J ASP.NEТ General Scripts Content ~ Online first.css Name: Рис. Поместим в файл Листинг hЗ 169 6.17. 6.23. first. css Создание таблицы стилей стили Содержимое файла CSS из листинга first. css из папки CSS 6.17. wwwroot/css { fon t-size: 18pt; font-family: sans -serif; td { border: 2рх solid Ыасk; border -collapse:co llap se; padding: Spx; font-family: sans-serif; t аЫе , Повторим процесс для создания в папке по имени Листинг second. css 6.18. wwwroot / css еще одной таблицы стилей с содержимым , приведенным в листинге Содержимое файла second. css из папки 6 . 18. wwwroot/css р f ont -fami ly: sans-serif; font-size: l Opt ; color: darkgreen; background-co lor : a n tiquewhite; borde r: lpx so l id Ыасk; padding: 2 р х; По соглашению специальные файлы JavaScript хранятся в папке wwwroot/ js. JavaScript File (Файл 6.24). Шаблон JavaScript File находится Создадим такую папку и с применением шаблона элемента JavaScript) добавим в нее файл thi rd. j s (рис. в разделе ASP.NET CoreqWebqScripts (ASP.NET СоrеqВеб q Сценарии) . 170 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 I" lnst•llod ASP . NEТ " @ Core Codt ~· ~· Gentral А Web ASP . NEТ General r.r S<npts Conttnt Г" "Online (i>'J JavaXnpt f1le ASP.NET Core Typ6cr1pt File ASP.NET Core TypeS<ript JSX File ASP . NEТ Core TypeS<:ript JSON Configuration File ASP.NEТ Core JSXFile ASP.NEТ Cort Angul,rJs Controller ASP.NEТ Core Angul11rJs Controller using Sscope ASP.NET Core AngularJs D1rectrve ASP.NET Core №me: Рис. 6.24. Создание файла Поместим в новый файл простой код Листинг 6.19. Содержимое файла JavaScript JavaScript, third. j s представленный в листинге из папки 6. 19. wwwroot/ j s document.addEventListener("DOMContentLoaded", function () var element = documen t . crea teElement( "p"); element.textContent = "This is the element from the third.js file"; document. querySelector ( "body") . appendChild ( element) ; ) ) ; Нам необходим еще один файл имени fourth. j s Листинг 6.20. JavaScript. Создадим в папке с содержимым, показанным в листинге Содержимое файла fourth. js из папки wwro ot/j s файл по 6.20. wwwroot/js document.addEventListener("DOMContentLoaded", function () var element = document . cre ateEl ement ( "р"); element.textContent = "This is the element from the fourth. js file"; documeлt. querySelector ( "b ody " ). appendChild (element) ; }) ; Обновление представления Последний Index. cshtm l (листинг Листинг подготовительный шаг связан с обновлением для использования новых таблиц стилей CSS представления и файлов JavaScript 6.21) . 6.21. Добавление элементов script и link в файле Index. cshtml из папки Views/Home @model IEnumer aЫe<WorkingWithVisualStud i o @{ Layout = null; ) .M odels .Pr oduct> Глава 6. Работа с Visual Studio 171 < ! DOCTYPE h t ml > < html> <head> <meta name="viewport" content="width=device-wi d th" /> < title>Working with Visual St u d i o</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> <b o dy> <h 3>Produ c ts< / h 3 > <p>Reque s t Time : @Da teT ime. Now . ToSt ring ( "НН : mm : ss") < / р> < tаЫе > <thead > <tr ><t d >Name</td >< td >Pr ice </ td ></ tr > < /thead > <tbody> @forea c h (var р in Model ) <tr> < td >@p.Name </td> < td > @( $" ( р. Pr i с е: С2 } " ) < / td > </tr> </ tbody > < / tаЫе> < / body> </ html> В результате запуска примера приложения появится содержимое, приведенное на рис . CSS. Существующее содержимое было стилизовано по с редством таблиц стилей 6.25. а код JavaScript добавил новое содержимое. D Worlcing wilh Vrs~I S С Ф localhost 04405 Products l ~IR-eq-u-es-t~n-m-e~1-5~08~5~0~~~~~~~~~~~~~~~~~~~~~~~~1 1 Name Price Lifejacket f:48 95 Soccer ball f:19 50 Cornerflag f:34.95 11 !•" ••• " •••"•••"".J• •• ~IT=his=~==th=ee=l•=m=e=nt=f•=om==th=ei:ou=rth==js=fll=e====================================I Рис. 6.25. Выполнение примера приложения Часть 1. Введение в инфраструктуру ASP. NET Саге MVC 2 172 Пакетирование и минификация в приложениях MVC В настоящий момент есть четыре статических файла. и браузер должен делать че­ тыре запроса, чтобы получить эти статические файлы . К тому же каждый такой файл при доставке клиенту отнимает больше полосы пропускания, чем должен, поскольку содержит пробельные символы и имена переменных. которые являются содержатель­ ными для разработчика. но не имеют никакого значения для браузера . Объединение файлов одного и того же типа называетс я пакетированием. Уменьшение размеров файлов называется мин11фика4ией. Обе задачи выполня­ ются в приложениях ASP.NET Core MVC с помощью Visual Studio. расширения Bundler & Minifler (Упаковщик и минификатор) для Установка расширения Visua/ Studio Первый шаг заключается Tools~Extensions в установке расширения . and Updates (Сервис~Расширения Выберем пункт меню и обновления) и щелкнем на кате­ гории Online (Онлайновые). чтобы отобразить галерею доступных расширений Visual Studio. В поле поиска справа вверху введем слово Bun dler (рис. 6.26). Отыщем расши­ рение Bundler & Minifier и щелкнем на кнопке Download (Загрузить). чтобы добавить его в Visual Studio. Перезапустим Visual Studio для завершения процесса установки. bltl'SIOns ~nd Updates ? ' Sort Ьу. {Ititv.n -ce- - - = = i Online ~ -А Bundlt~ ~1' Bundler & Minifier lle~ Adds support for bundling .1nd minifying JJvaScript, Visu&I Studio M1rketplece [ Oown1oad] CS:S .1nd HTML filts in •ny projtct. " Controls to Templatn t> Tools Se.11ch Resutts Х Created Ьу: Mads Kristensen Version: 2.4.340 Downloods: 408992 Rollup Task Runner T1sk Runner Explorer support for RollupJs - The next 9ener11tion J1v1Script modult bundler. Roting: (89 Vot") Мо1е lnformat1on Report ble:nsion to Mюosoft ii Upd1tes 11 Ro1ming Ьtension Minager S<:he-duied For 11\Std: None S<heduled For Upclote: None S<:heduied For Uninsi.11: None CNngt your &l:tn~ions and Upd1t" ~ettings Рис. 6.26. Поиск расширения Visual Studio пакетирование и минификация файлов После установки расширения перезапустим Visual Studio и откроем проект при­ мера . Благодаря добавлению расширения появилась возможность выбирать в окне Solution Explorer множество файлов одного типа. пакетировать их вместе и минифи ­ цировать их содержимое. В качестве примера выберем файлы в окне first. c ss и seco nd. Solution Explorer. щелкнем на них правой кнопкой мыши и выберем в контекстном меню пункт Bundler & MinifierqBundle and Minify Files (Упаковщик и минификаторqПакетировать и минифицировать файлы) . как показано на рис . 6.27. css Глава 6. Работа с Visual Studio • t:i Solution Explorer ~ 173 Х ·l 'Ф·!;"G})! j )'-Р· Search Solutioi> Explorer (Ctr1•;) @wwwroot ~ css ~ Open js OpenWith". !J fourthJs !J third.js {} Cleanup Selected Cod• lib Collapse R..:ursively '0 Run Web Cod• Analysis Bundlor & Minifi•r Т ----- :- 1 ---- Bundle and Minify Files ~~ft+ Alt+~ N..-, Solut•on Explor<r Vi.-." Exclude From Proj•ct .)(, Cut Ctrl+X OJ Сору Ctrl+C х D•lel• D•I Renarne )' Рис. Alt+Enter Properties 6.27. Пакетирование и минификация файлов Сохраним выходной файл под именем рение обработает файлы CSS. bundle . css, В окне Solutioп Explorer CSS в результате чего расши­ отобразится новый элемент bundle. css, который можно раскрыть и увидеть минифицированный файл с именем bundle .min. css. Открыв минифицированный файл , вы заметите, что содержимое обоих отдельных файлов CSS было объединено. а все пробельные символы удалены. Работать с таким файлом напрямую вы вряд ли захотите . но он меньше по размеру и требует только одного НТГР- подключения для доставки стилей Повторим процесс с файлами bundle . j s и bundle. min. j s в third. j s и fourth. j s. папке wwwroot/ j s. CSS клиенту. чтобы создать новые файлы Внимание! Удостоверьтесь, что выбираете файлы в порядке их загрузки браузером, чтобы предохранить порядок следования стилей или операторов кода в выходном файле. Таким образом , например , выбирайте файл th ird. j s перед файлом f ou r t h. j s, чтобы обес­ печить выполнение кода в правильном порядке. В листинге 6 .22 показано представление Index. cshtml, в котором элементы link для отдельных файлов заменены одним таким элементом, запрашивающим пакетиро­ ванные и минифицированные файлы. Листинг 6.22. Применение пакетированных и минифицированных файлов в файле Index. csh tml из папки Views/Home @model I En umeraЫe<Work ingWi t h Vis ualStudio.M od els.Produ ct> @{ Layout = n u ll ; ) < ! DOCTYPE html > <html > <head> Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 174 <meta name="viewport" content="width=device-width" /> <title>Working with Visual Studio</title> <link rel="stylesheet" href="css/bundle.min.css" /> <script src="js/bundle.min.js"></script> </head> <body> <h3>Products</h3> <p>Request Time: @DateTime. Now. ToStr ing ( "НН: mm: ss") </р> <tаЫе> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> <tbody> @foreach (var р in Model) <tr> <td>@p.Name</td> <td>@($"{p.Price:C21")</td> </tr> </tbody> </tаЫе> </body> </html> После запуска приложения никаких визуальных отличий не будет заметно, но приложение использует пакетированные и минифицированные файлы. предоставляя браузеру все стили и код, которые были определены в отдельных файлах. По мере выполнения операций пакетирования и минификации расширение ведет учет обработанных файлов в файле по имени bundleconfig. j son. находящемся в корневой папке проекта. Вот конфигурация, которая была сгенерирована для файлов в примере приложения: "outputFileName": "wwwroot/css/bundle.css", "inputFiles": [ "wwwroot/css/first.css", "wwwroot/css/second.css" 1. { "outputFileName": "wwwroot/js/bundle.js", "inputFiles": [ "wwwroot/js/third.js", "wwwroot/js/fourth.js" Расширение автоматически отслеживает входные файлы на предмет изменений и заново генерирует выходные файлы, когда происходят изменения. гарантируя от­ ражение в пакетированных и минифицированных файлах любых модификаций. Для демонстрации в листинге 6.23 приведено изменение, внесенное в файл third. j s. Глава 6. Работа с Visual Studio Листинг 175 6.23. Внесение изменения в файл third. js из папки wwwroot/js d o cument.addEventListener("DOMContentLoaded", f u nction () ( v ar element = document. createE l ement ( "р" ) ; element. textContent = "This is the element from the (modified) third. js file"; document. querySelector ( "body" ) . appendChild (el ement); )) ; После сохранения файла расширение заново сгенерирует файл Перезагрузив страницу в браузере. можно увидеть изменение (рис. CJ Working with V1Sual Stu. С bundle .min. j s. 6.28). Х 1Ф localhost 64_405 *J Products jRequesl Time 15 51 08 Name Price Lifejacket f:48.95 Soccer ball f:19.50 Corner flag ЕЗ4 . 95 jThis is lhe element from the (modified) third.js file IThis is the element from the fourlh js file Рис. 6.28. Обнаружение изменений в пакетированных и минифицированных файлах Резюме В главе были описаны возможности, которые Visual Studio предлагает для раз­ работки веб-приложений, включая автоматическую компиляцию классов, средство Browser Link, а также пакетирование и минификацию. В следующей главе объясняет­ ся, как подвергать модульному тестированию проекты ASP.NET Core MVC. ГЛАВА 7 Мо,пульное тестирование приложений МVС главе будет показано, как проводить модульное тестирование при­ в настоящейMVC. ложений Модульное тестирование это вид тестирования, при котором индивидуальные компоненты изолируются от остальной части приложения, позволяя тщательно проверить их поведение. Инфраструктура ASP.NET Core MVC была спро­ ектирована так, чтобы облегчить задачу создания модульных тестов, и среда Studio Visual предлагает поддержку широкого диапазона инфраструктур модульного тести­ рования. В главе объясняется, как настроить проект модульного тестирования, каким образом установить одну из самых популярных инфраструктур тестирования и что собой представляет процесс написания и прогона тестов. В табл. 7.1 приведена свод­ ка по главе. Таблица 7.1. Сводка по rлаве Задача Создание модульного теста Реwение Листинг Создайте проект модульного тестиро­ 7.5, 7.6 вания, установите тестовый пакет и до­ бавьте классы, которые содержат тесты Изоляция компонентов для модуль­ Используйте интерфейсы для разделения ного тестирования компонентов приложения и применяйте в 7.7-7.14 модульных тестах фиктивные реализации с ограниченными тестовыми данными Прогон тех же самых тестов xUпit.пet е разными значениями данных Используйте параметризованный модульный тест либо получайте тестовые 7.15-7.17 данные из метода или свойства Упрощение процесса создания фик­ тивных тестовых объектов Применяйте инфраструктуру имитации 7.18, 7.19 177 Глава 7. Модульное тестирование приложений MVC Проводить ли модульное тестирование? Наличие возможности легко выполнять модульное тестирование является одним из преиму­ ществ использования инфраструктуры ASP.NEТ Core MVC, но такая процедура предназначе­ на не для всех, и я не намерен притворяться, что дело обстоит иначе. Мне нравится модульное тестирование, и я применяю его в своих проектах, но далеко не во всех и не настолько согласованно, как вы могли ожидать. Я предпочитаю концентрироваться на написании модульных тестов для средств и функций, о которых знаю, что они будут труд­ ны в реализации и вполне вероятно станут источником ошибок после развертывания. В та­ ких ситуациях модульное тестирование помогает структурировать мои мысли о том, как луч­ ше всего реализовать то, что необходимо. Я считаю, что одно лишь размышление о том, что нужно тестировать, способствует появлению идей относительно потенциальных проблем, причем до того, как доведется иметь дело с действительными ошибками и дефектами. Тем не менее, модульное тестирование - инструмент, а не догма, и только лично вы зна­ ете, в каком объеме оно должно проводиться. Если вы не находите модульное тестирова­ ние полезным или располагаете другой методологией, которая вам больше подходит, то не должны думать, что обязаны использовать модульное тестирование просто потому, что это модно. (Однако если вы не располагаете более эффективной методологией и не тестируете вообще, то вероятно даете возможность пользователям обнаруживать имеющиеся ошибки, что редко оказывается идеальным решением. Вы не обязаны выполнять модульное тести­ рование, но на самом деле должны проводить тестирование какого-то вида.) Если вы не сталкивались с модульным тестированием ранее, тогда я предлагаю опробовать его и посмотреть, как оно работает. Если вы не поклонник модульного тестирования, то мо­ жете пропустить данную главу и перейти к чтению главы 8, где начнется построение более реалистичного приложения MVC. Подготовка проекта для примера В главе мы продолжим пользоваться проектом рый был создан в главе объектов Product 6. WorkingWithVisualStudio, кото­ Здесь мы добавим в него поддержку для создания новых в хранилище. Включение встроенных вспомогательных функций дескрипторов В данной главе применяется одна из встроенных вспомогательных функций де­ скрипторов для установки атрибута href элемента а. Работа вспомогательных функций дескрипторов подробно объясняется в главах 23-25, но пока мы просто должны включить их. Создадим файл импортирования представлений, щелкнув пра­ Views, выбрав в контекстном меню пункт Addt:::>New ltem (Добавитьо:::>Новый элемент) и указав шаблон элемента MVC View lmports Page вой кнопкой мыши на папке (Страница импортирования представлений МVС) из категории Studio автоматически назначит файлу имя кнопке Add Среда Visual а щелчок на (Добавить) приведет к его созданию. Поместим в файл содержимое. пока­ занное в листинге Листинг ASP.NET. Viewimports. cshtml, 7 .1. 7. l. Содержимое файла _ Viewimports. csh tml из папки @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Views Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 178 Оператор в листинге 7.1 включает встроенные вспомогательные функции дескрип­ торов, в том числе ту, которая вскоре будет использоваться в представлении Можно было бы добавить операторы using Index. для импортирования пространств имен из проектов, но представления не являются важной частью рассматриваемого здесь примера приложения. так что ссылка на типы моделей с их пространствами имен проблем не вызовет. Добавление действий к контроллеру Первый шаг связан с добавлением действий к контроллеру Home. который будет визуализировать представление для ввода данных и для получения этих данных от браузера (листинг главе 2 Листинг 7.2). Действия следуют тому же шаблону. который применялся в и подробно обсуждается в главе 7.2. 17. Добавление методов действий в файле HomeController. сз из папки Controllers using Microsoft.AspNetCore.Mvc; using WorkingWithVisualStudio.Models; using System.Linq; namespace WorkingWithVisualStudio.Controllers puЫic class HomeController : Controller { SimpleRepository Repository puЫic = SimpleRepository.SharedRepository; IActionResul t Index () => View (Reposi tory. Products . Where (р => р?. Price < 50)) ; [HttpGet] IActionResul t AddProduct () puЬlic => View (new Product () ) ; [HttpPost] puЬlic 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; } <!DOCTYPE html> <html> Глава 7. Модульное тестирование nриложений MVC 179 <head> <meta name="viewport" content="width=device-width" /> <title>Working with Visual Studio</title> <link rel="stylesheet" href="/liЬ/bootstrap/dist/css/bootstrap.min.css" /> </head> <body class="p-2"> <hЗ class="text-center">Create Product</hЗ> <form asp-action="AddProduct" method="post"> <div class="form-group"> <label asp-for="Name">Name:</label> <input asp-for="Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Price">Price:</label> <input asp-for="Price" class="form-control" /> </div> <div class="text-center"> <button type="submit" class="Ьtn Ьtn-primary">Add</button> <а asp-action="Index" class="btn btn-secondary">Cancel</a> </div> </form> </body> </html> Представление содержит НТМL-форму. которая применяет НТТР-запрос для отправки значений Narne и Price действию Содержимое стилизовано с использованием пакета AddProduct CSS из контроллера POST Horne. Bootstrap. Обновление представления rndex Последний подготовительный шаг предусматривает обновление представления Index, чтобы включить в него ссылку на новую форму (листинг 7.4). Кроме того, поя­ вилась возможность удалить файлы JavaScгipt. используемые в предыдущей главе, и заменить специальные таблицы стилей ся к элементам Листинг 7.4. HTML в CSS стилями Bootstrap, которые применяют­ представлении. Обновление содержимого в файле Index.cshtml из папки Views/Home @model IEnumeraЫe<WorkingWithVisualStudio.Models.Product> @( Layout = null;} <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Working with Visual Studio</title> <link rel="stylesheet" href="/liЬ/Ьootstrap/dist/css/Ьootstrap.JD.in.css" </head> <body class="p-1"> <hЗ class="text-center">Products</hЗ> <taЬle class="taЬle taЬle-bordered taЬle-striped"> <thead> <tr><td>Name</td><td>Price</td></tr> </thead> /> Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 180 < tb od y > @forea c h (var р in Mo del) <tr > <td> @p.Name</td > <td>@( $" {p.Pri c e: C2) " ) </ t d> < / tr > </tbod y> < /tаЫе> <div class="text-center"> <а class="btn Ьtn-primary" asp-action="AddProduct"> Add New Product </а> </div> < /body> </ html> Запустив пример приложения, можно увидеть по-новому стилизованное содержи ­ мое и кнопку Add New Product (Добавить новый товар). щелчок на которой приводит к открытию формы для ввода данных. Отправка формы добавит в хранилище новый объект Pr oduct и перенаправит браузер для отображения первоначального представ­ ления приложения (рис . 7. 1) . DWattUfl9wtttlVl\~I~ С )( Ф loc11hot1 б4АО5 tr ; Products Create Product н"m, 1 / l•f•J•cke< Soccer н .... Price lifej&ekt\ S<S.95 So<:cer Ьаt! $19.SO Corner flj]g SЗ4.95 Runntng Shoes b~ll Comtrfl>g 48 50 S"8.50 +Ш§iij§i -l Рис. 7 .1. Выполнение примера приложения Совет. Не забывайте, что в этом примере объекты хранятся только в памяти, т.е . любые со­ здаваемые товары при перезапуске приложения будут утеряны. Модульное тестирование приложений MVC Модульны е тесты используются для проверки поведения отдельных компонентов и функциональных средств в приложении, а платформа тура MVC ASP.NET Core и инфраструк­ спроектированы так. чтобы максимально облегчить настройку и запу с к модульных тестов для веб-приложений. В последующих разделах объясняется, как настроить модульное тестирование в Visual Studio. и демонстрирует с я написание Глава 7. Модульное тестирование приложений MVC модульных тестов для приложений MVC. 181 Кроме того, будут представлены полезные инструменты, которые делают модульное тестирование проще и надежнее. Доступен целый ряд разных пакетов для модульного тестирования. В книге приме­ xUnit.net; он выбран из-за хорошей интегра­ Visual Studio, к тому же данный пакет использовался командой разработчиков Microsoft при написании модульных тестов для ASP.NET Core. В табл. 7.2 приведена сводка, позволяющая поместить xUnit.net в контекст. няется один из них, который называется ции с Таблица 7.2. Помещение xUnit.net в контекст Вопрос Ответ Что это такое? xUnit.net - это инфраструктура модульного тестирования, которая может применяться для тестирования приложений ASP.NET Соге MVC Чем она полезна? xUnit.net - хорошо написанная инфраструктура модульного тести­ рования, которая легко интегрируется со средой Как она используется? Visual Studio Тесты определяются как методы, аннотированные с помощью атри­ бута Fact или Theory. Внутри тела метода используются методы Assert для сравнения ожидаемого результата теста с тем, класса что получилось в действительности Существуют ли какие­ Главный просчет при модульном тестировании связан с недоста­ то скрытые ловушки точной изоляцией тестируемого компонента. За дополнительными или ограничения? сведениями обращайтесь в раздел "Изолирование компонентов для модульного тестирования" далее в главе. Самой крупной проблемой, специфичной для xUnit.net, является нехватка документации. Кое-какая базовая информация доступна на веб-сайте xuni t. gi thub. io, h t tp: / / но расширенное применение требует метода проб и ошибок Существуют ли Доступно множество инфраструктур модульного тестирования. альтернативы? Двумя популярными альтернативами являются тва Microsoft) и MSTest (производс­ NUnit На заметку! Практически все в области модульного тестирования является предметом пер­ сональных предпочтений и вопросом шумных разногласий. Некоторым разработчикам не нравится отделять модульные тесты от кода приложения, и они предпочитают определять тесты в том же самом проекте или даже в том же самом файле класса. Я описываю здесь распространенный подход, которому следую сам, но если он не кажется вам правиль­ ным, тогда вы должны поэкспериментировать с различными стилями тестирования, пока не подберете для себя подходящий. Создание проекта модульного тестирования Для приложения ASP.NET Core обычно создается отдельный проект Visual Studio, содержащий модульные тесты, каждый из которых определяется как метод в классе С#. Применение отдельного проекта означает, что приложение можно развернуть, не развертывая одновременно тесты. Чтобы создать проект тестирования. щелкнем правой кнопкой мыши на элементе решения WorkingWithVisualStudio тном меню пункт Addo::>New Project в окне Solution Explorer и выберем в контекс­ (Добавить<::> Новый проект). 182 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 В категории Visual C#G.NET Саге выберем шаблон xUnit Test Project (.NET Core) (.NET Core)). как показано на рис. 7.2. (Проект тестирования xUпit @E1mowort4.б.1 Р Rec~nt • lnst1lled :\ =J So11Ьy: (D=rf=•"'u~'------·_,l Console Арр (.NЕТ Cort) Visu1IC~ WindO'Ns Classic Desktop Yleb CI•" Library (.NET Со") Visual (J" .NEТCore " UnitTest Project (.NET Core) Visual (:- @ ASP.NEТ Cor. W.Ь Apploc1tion .NET Stond•rd Visual с~ project th1t cont.ains xUn1t.net ttsts th1t can run on .NH Core on Windows. Unux 1nd MocOS. ~ ~~~~.; i~; i>;~;~ (.Nй"с6 ••>· Cloud Test ~ Е: Seorch(Ctrl•E) А ... Visual (# о- 11' Туре: Visuo!ll 81s1c SQl S.rver Visual с: Ontine Not finding whlt you are looking for? Open V1su•I Stud10 lмttlfer WorkingWrthVisu.!IStudio.Tests №mo: @"i:ojtcts\Wortin9W~hVisu11Studio locat1on: Рис. 7.2. Browse." Выбор шаблона xUnit Test Project (.NET Core) Внимание! Удостоверьтесь в том, что выбираете правильный шаблон проекта. В Visual Studio предлагается несколько шаблонов для проектов модульного тестирования, которые име­ ют похожие названия. По соглашению проекту модульного тестирования назначается имя <ИмяПрило ­ жения>. Tests. Установим имя нового проекта в на кнопке ОК. Среда Visual Studio Wor kingWi th Visua .lStudi o . Tests и щелкнем NuGet для ин ­ создаст проект и установит пакеты фраструктуры xUпit.пet и ее зависимостей. Удаление стандартных тестовых классов Среда Visual Studio добавляет в проект тестирования файл класса С#. который приведет в беспорядок результаты более поздних примеров . Щелкнем правой кноп­ кой мыши на файле Uni tTest l. cs в проекте Work ingWi thVisua.l Studio . Test s и Delete (Удалить). В открывшемся диалоговом окне Visual Studio удалит указанный файл класса . выберем в контекстном меню пункт щелкнем на кнопке ОК. и Создание ссылки на проект Чтобы сделать классы в главном проекте доступными для тестирования. щелкнем правой кнопкой мыши на элементе WorkingWi thVisua.l St udio. Tests в окне Solution AddGReference (ДобавитьGСсылка). Отметим флажок для элемента Worki ngWithVi sua.lSt udio в разделе Solution (Решение). как иллюстрируется на рис. 7.3 . Explorer и выберем в контекстном меню пункт Глава 7. Модульное тестирование приложений MVC .:Refe1ence Ma~ager • WorkingW1thVi~ualStudio.Tests . . .' · Projects А ~ame ~ Shared Projects ~ Browse Path ../Cllt~~'lФ~·m••·~2zmп1~•11m.~ВФ1!1·•~··•••а·~·· lt;Б Р· Name: WorkingWithVisualStudio 1 Browse." Рис. Х ? Search (Ctrl+ Е) Solution 183 j L-= ~к. ~11 Cancel Создание ссылки на проект приложения 7.3. Щелкнем на кнопке ОК для создания ссылки на проект приложения. В окне Solutioп Explorer может быть виден работающий значок, отображающийся на элемен­ те Depeпdeпcies (Зависимости) для проекта тестирования , но после построения про ­ ектов он исчезнет. Написание и выполнение модульных тестов Теперь, когда все подготовительные шаги завершены, можно приступать к напи ­ санию некоторых тестов. Первым делом добавим в проект Wo гkingWi thVisual Studi o са по имени Pгoduct.Tests . cs . Tests файл клас ­ с определением, представленным в листинге 7.5. Несмотря на простоту , класс содержит все, что требуется для начал а модульного тестирования . На заметку! В методе CanChang e Pгod uc tPгic e ( ) умышленно допущена ошибка, кото­ рая позже в разделе будет исправлена. Листинг 7.5. Содержимое файла ProductTests. cs WorkingWithVisualStudio.Tests из папки usi ng Wor kin g Wi th Vi s u a l St ud io . Mode l s ; using Xun it ; namespace WorkingWithVisualStudi o .Te sts puЫic c la ss Pr o du ct Tests { [Fact ] puЫi c v oid CanCha ngeProduct Name() 11 Орга ни заци я v ar 11 p.Name 11 n e w Prodl!c t { Name р = = "T est " , Pri ce Де й ств и е = "New Name "; Утвержден и е Asse rt .Equa l ( "New Name", p . Name); [ Fact ] puЫi c void Ca nCh angeP r o du ct Price() { l OOM }; Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 184 11 Организация var 11 р = new Product { Name p.Price = 11 "Test", Price lOOM ); Действие 200М; Утверждение Assert.Equal(lOOM, p.Price); В классе ProductTes ts присутствуют два модульных теста, проверяющих отдельные Product из проекта WorkingWi thVisualStudio. линии поведения класса модели Проект тестирования может содержать много классов, которые способны включать много модульных тестов. По соглашению имя тестового метода описывает то, что делает тест, а имя клас­ са - то, что подвергается тестированию. Это облегчает структурирование тестов в проекте и упрощает понимание того, какими будут результаты всех тестов, когда они прогоняются средой тесты для класса Visual Studio. Product, Имя ProductTests можность изменения названия и цены объекта Атрибут Fact указывает, что класс содержит а имена методов говорят о том, что они проверяют воз­ Product. применяется к каждому методу, указывая на то, что метод является тестом. Внутри тела метода модульный тест следует шаблону, который называется организация/действие/утверждение (arrange/act/assert -А/А/А). Организация от­ - к выполнению теста, а утверж­ носится к настройке условий для теста, действие дение - к проверке того, что результат оказался тем. который ожидали. Разделы организации и действия этих тестов представляют собой обычный код С#, но раздел утверждения обрабатывается инфраструктурой Assert, доставляет класс по имени xUnit.net, которая пре­ чьи методы используются для проверки. является ли результат действия тем, что ожидался. Совет. Атрибут Fact и класс Assert определены в пространстве имен должен быть предусмотрен оператор Методы класса Assert using Xuni t, для которого в каждом тестовом классе. определены как статические и применяются для выполне­ ния разных видов сравнений между ожидаемыми и действительными результатами. В табл. 7.3 описаны распространенные методы класса Каждый метод класса Assert Assert. позволяет выполнять разные виды сравнений и генерирует исключение, если результат оказывается не тем, который ожидался. Исключение применяется для указания на то, что тест не прошел. В тестах из лис­ тинга 7.5 метод Equal () использовался для определения. корректно ли изменилось значение свойства: Assert.Equal("New Name", p.Name); Глава 7. Модульное тестирование приложений MVC Таблица 7 .3. Часто используемые методы класса 185 Assert из инфраструктуры xUnit.net Метод Описание Equal(expected, resul t) Добавляет утверждение о том, что результат равен ожидаемо­ му исходу. Существуют перегруженные версии этого метода для сравнения различных типов и для сравнения коллекций. Имеется также версия, которая принимает дополнитель- ный аргумент в форме объекта, реализующего интерфейс IEqualityComparer<T> для сравнения объектов NotEqual(expected, result) Добавляет утверждение о том, что результат не равен ожидае­ True(result) Добавляет утверждение о том, что результат равен t rue False (result) Добавляет утверждение о том, что результат равен false I s Type(expected, result) Добавляет утверждение о том, что результат принадлежит IsNotType(expected, result) Добавляет утверждение о том, что результат не принадлежит IsNull (result) Добавляет утверждение о том, что результат равен IsNotNull(result) Добавляет утверждение о том, что результат не равен InRa nge (result, l ow, high) Добавляет утверждение о том, что результат находится между No tinRange(resul t , l ow, high) Добавляет утверждение о том, что результат не находится Throws(exception, expre s sion) Добавляет утверждение о том, что указанное выражение гене­ мому исходу указанному типу указанному типу low и между null null high l ow и high рирует исключение заданного типа Прогон тестов с помощью окна Test Explorer Среда Visual Studio преДТiагает поддержку ДТIЯ поиска и прогона модульных тестов посредством окна Test Explorer (Проводник тестов), которое доступно через пункт меню Testc:::>Windowsc:::>Test Explorer (Тестс:::>Окнас:::>Проводник тестов) и показано на рис. 7.4. •CJX itst Explore.r ___________ Test Explorer l,.a.:; •.;l; ~=;.·.;5;..S ;,; •' ;" .;. " (-• Н: .3 •DX Р· Sei!lrth Rtm Atl 1 Run~. .... 1 f)t.,yfist : АИ T~ts .., ~ Failed Tests (1) О Working\V1thVisui!llStudio.Tests.ProductTests.(4nChangeProductPnce 11 ms ---------1-- А Pиsed Tests (1) (# WorkingWithV1sualStudio.T~ts.ProductTests.CanChi!lngeProductN!me Summary t..rt Test Run fallod (Тot•I Run Time 0:0001.9799989) ~ 1 Test F4ifed ti 1 Test P4ssed Рис. 7.4. Окно Test Explorer в Visual Studio 4 ms 186 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Совет. Если вы не видите модульные тесты в окне Test Explorer, тогда постройте реше­ ние. Компиляция инициирует процесс, с помощью которого обнаруживаются модульные тесты. Выполним тесты. щелкнув на пункте Ruп Среда Vlsual Studio применит xUnit.net зультаты. Как отмечалось, тест All (Выполнить все) в окне Test Explorer. для прогона тестов в проекте и отобразит ре­ CanChangeProductPrice () содержит ошибку, ко­ торая приводит к тому, что он не проходит. Проблема связана с аргументом метода Assert. Equal (), Price, а не со тва из-за чего происходит сравнение с исходным значением свойс­ значением, на которое оно изменилось. В листинге 7.6 проблема устранена. Совет. Когда тест не проходит, то прежде чем просматривать компонент, на который он на­ целен, всегда полезно проверить правильность самого теста, особенно если тест только что написан или недавно модифицировался. Листинг 7.6. Исправление теста в файле Productтests. cs из папки WorkingWithVisualStudio.Tests using WorkingWithVisualStudio.Models; using Xunit; narnespace WorkingWithVisualStudio.Tests class ProductTests { puЫic [Fact] puЫic 11 Организация var 11 void CanChangeProductNarne() р = new Product { Narne = "Test", Price p.Narne 11 lOOM }; Действие = "New Narne"; Утверждение Assert.Equal("New Narne", p.Narne); [Fact] puЫic 11 Организация var 11 void CanChangeProductPrice() { р = = "Test", Price lOOM }; Действие p.Price 11 new Product { Narne = 200М; Утверждение Assert.Equal(200M, p.Price); } При наличии большого количества тестов выполнение их всех может занять неко­ торое время. Таким образом, чтобы можно было работать быстро и итера'tивно, в окне Глава 7. Модульное тестирование приложений MVC Test Explorer 187 предлагаются различные варианты для выбора подмножества тестов. подлежащих прогону. Самым полезным подмножеством является набор тестов. кото­ рые не прошли (рис. 7.5). Запустим исправленный тест снова. и в окне Test Explorer будет показано, что не прошедшие тесты отсутствуют. Tost Explorer \:~ • Ei Seшh Run А11 1 Run. "' Failod Т 0Tests А Passed 1 $ Tests) 1 P!aylJ>t: All Tests • Run f•iled Tests 11 ... р Run Not Run Tests б Run P•ssed Tests Tests.ProductTests.CanChangeProductPrrce • Ropoat Last Run , ~ С• 0 1 ems ". Ctrl+R. L Surпmary Summary Last Tm Run Failed (Total Run T1me 0;00:04} О 1 Test Failed & 1 Test P•ssed Рис. Last Tffi Run Passed (ТоU.1 Run T1me 0;00:02} 1 Test P•ssed $ 7.5. Избирательный прогон тестов Изолирование компонентов для модульного тестирования Написание модульных те стов для клас с ов моделей вроде тью не отличается. Класс Produc t Produ c t особой сложнос­ не тол ько прост. он также самодостаточен, т.е. при выполнении какого-то действия над объектом Pr o d uct можно иметь уверенность в том, что тестируется функциональность. предоставля е мая классом Product. С другими компонентами в приложении МVС ситуация сложнее, потому что между ними есть зависимости. Следующий набор определяемых тестов будет оперировать на контроллере . исследуя последовательность объектов Product , которая передается между контр оллером и пр едставлением . При сравнении объектов, являющихс я э кземплярами специальных классов , пона­ добится использовать метод Assert. Equa l () из xUnit.net, IEqu a li tyCornpar er< T>, гумент, реализующий инт е рфейс который принимает ар­ так что объекты можно сравнивать . Сн а чала необходимо добавить в проект модульного те стирования файл класса по имени Cornpa r er . cs классов, приведенные в листинге Листинг и поместить в него определения вспомогательных 7.7. 7. 7. Содержимое файла Comparer. cs из папки WorkingWithVisualStudio . Tests using System ; using System.Co l lec t ions . Ge n e ri c ; Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 188 namespace WorkingWithVisualStudio.Tests ( class Comparer ( static Comparer<U> Get<U>(Func<U, U, bool> func) return new Comparer<U>(func); puЫic puЫic ( class Comparer<T> : Comparer, IEqualityComparer<T> private Func<T, Т, bool> comparisonFunction; puЫic Comparer(Func<T, Т, bool> func) func; comparisonFunction puЫic bool Equals (Т х, Т у) ( return comparisonFunction(x, ( puЫic у); int GetHashCode(T obj) return obj .GetHashCode(); puЫic Показанные классы позволят создавать объекты IEqualityComparer<T> с при­ менением лямбда-выражений вместо определения нового класса для каждого типа сравнения. которое нужно предпринимать. Это не является жизненно необходимым. но упростит код в классах модульных тестов и сделает его легче для понимания и сопровождения. Теперь. когда можно легко делать сравнения, давайте рассмотрим проблему зави­ симостей между компонентами в приложении. Добавим в проект имени WorkingWi thVisualStudio. Tests новый файл класса по HomeControllerTests. cs и поместим в него определение модульного теста. представленное в листинге Листинг 7.8. Содержимое файла из папки using using using using using 7.8. HomeControllerTests. cs WorkingWi thVisualStudio. Tests Microsoft.AspNetCore.Mvc; System.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xunit; namespace WorkingWithVisualStudio.Tests puЫic class HomeControllerTests ( [Fact] puЫic void IndexActionModellsComplete() / / Организация var controller = new HomeController(); 11 Действие var model = (controller.Index() as ViewResult) ?.ViewData.Model as IEnumeraЬle<Product>; Глава 7. Модульное тестирование nриложений MVC 11 189 Утверждение Assert.Equal(SiтpleRepository.SharedRepository.Products, (pl, р2) => && pl.Price == p2.Price)); Coтparer.Get<Product>( Модульный тест в листинге 7.8 pl.Naтe == тodel, р2.Nате проверяет, что метод действия Index () передает представлению все объекты в хранилище. (На раздел действия можно пока не обра­ щать внимания; класс ViewResul t будут объясняться в главе 17. и роль, которую он играет в приложениях MVC, В настоящий момент достаточно знать, что здесь полу­ чаются данные модели, возвращаемые методом действия Index () .) Запустив тест, можно заметить. что он не проходит, указывая на отличие между на­ бором объектов в хранилище и набором объектов, которые возвратил метод Index () . Однако когда дело доходит до выявления причины, из-за чего тест не прошел, воз­ никает проблема: предполагается, что тест действует на контроллере Ноте, но класс контроллера зависит от класса SiтpleRepository. Данный факт затрудняет выяс­ нение, отразил ли тест проблему с классом, на который он нацелен, или же проблему в другой части приложения. Пример приложения достаточно прост, чтобы можно было легко выявить про­ блему, всего лишь взглянув на код классов HomeController и SimpleRepository. В реальном приложении визуальный осмотр не настолько прост, т.к. цепочка зави­ симостей способна затруднить понимание того, что привело к отказу в прохождении теста. Обычно хранилище будет полагаться на какую-то разновидность системы пос­ тоянного хранения, подобную базе данных, а также библиотеку, которая предоставля­ ет к ней доступ. Модульный тест может взаимодействовать со всей цепочкой сложных компонентов, любой из которых может вызвать проблему. Модульные тесты эффективны, когда они нацелены на небольшие части приложе­ ния, такие как отдельный метод или класс. Нам необходима возможность изоляции контроллера Ноте от остальной части приложения, чтобы можно было ограничить область действия теста и исключить влияние со стороны хранилища. Изолирование компонента Ключом к изолированию компонентов является использование интерфейсов С#. Чтобы отделить контроллер от хранилища, добавим в папку файл по имени листинге Листинг IReposi tory. cs 7.9. 7.9. Содержимое файла IRepository.cs using System.Collections.Generic; namespace WorkingWithVisualStudio.Models interface IRepository { Products { get; ) void AddProduct(Product р); puЫic IEnumeraЫe<Product> Models новый с определением интерфейса, которое показано в из папки Мodels 190 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC 2 С интерфейсом IRepository не связано ничего примечательного (за исключе­ нием того, что в нем не определен полный набор операций, который обычно будет нужен в веб-приложении; более реалистичный и завершенный пример можно найти в главе 8). Тем не менее, добавление интерфейса вроде IRepository позволяет лег­ ко изолировать компонент для тестирования. Первым делом нужно обновить класс SimpleReposi tory. Листинг 7. 1О. чтобы он реализовывал новый интерфейс (листинг Реализация интерфейса в файпе Si.mpleReposi tory. cs 7.10). из папки Model s using System.Collections.Generic; namespace WorkingWithVisualStudio.Models puЫic class SimpleRepository : IRepository private static SimpleRepository sharedRepository SimpleRepository(); private Dictionary<string, Product> products = new Dictionary<string, Product>(); puЫic new static SimpleRepository SharedRepository => sharedRepository; puЫic SimpleRepository() { var initialitems = new[] { newProduct{Name "Kayak", new Product { Name "Lifejacket", new Product { Name "Soccer ball", new Product { Name "Corner flag", Price=275M ), Price 48.95М ), Price 19.50М ), Price 34.95М) ); foreach (var р in initialltems) { AddProduct(p); products.Add("Error", null); puЫic IEnumeraЬle<Product> puЬlic Products => products.Values; void AddProduct(Product р) => products.Add(p.Name, р); Следующий шаг предусматривает модификацию контроллера. чтобы свойство, применяемое для ссылки на хранилище, использовало интерфейс, а не класс (лис­ тинг 7. l l). Совет. Инфраструктура ASP.NET Core MVC поддерживает более элегантный подход к реше­ нию такой проблемы, который называется внедрением зависимостей и описан в главе 18. Внедрение зависимостей зачастую вызывает путаницу, поэтому в настоящей главе компо­ ненты изолируются более простым и ручным способом. Листинг 7.11. Добавление свойства из папки Reposi tory Controllers using Microsoft.AspNetCore.Mvc; using WorkingWithVisualStudio.Models; using System.Linq; в файле HomeController. cs Глава 7. Модульное тестирование приложений MVC 191 narnespace WorkingWithVisualStudio.Controllers { puЫic class HorneController : Controller { = SiшpleReposi tory. SharedReposi tory; puЬlic IReposi tory Reposi tory puЫic IActionResult Index() => View(Repository.Products . Where (р => р?. Price < 50) ) ; [HttpGet] IActionResult AddProduct() => View(); puЫic [HttpPost] IActionResult AddProduct(Product Repository.AddProduct(p); return RedirectT0Action( 11 Index 11 ) ; puЫic р) { Модификация может выглядеть незначительной, но она позволяет изменять хра­ нилище, которое контроллер применяет во время тестирования. что демонстрирует способ изоляции контроллера. В листинге 7 .12 модульные тесты контроллера обнов­ лены, так что они используют специальную версию хранилища. Листинг 7 .12. Изоляция контроллера в модульном тесте внутри файла HomeControllerTests. cs using using using using using из папки Controllers Microsoft.AspNetCore.Mvc; Systern.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xuni t; narnespace WorkingWithVisualStudio.Tests puЫic class HorneControllerTests { class ModelCoшpleteFakeRepository puЬlic IEnwneraЫe<Product> new new new new Product Product Product Product { { { { Nаше Nаше Nаше Nаше = = = = 11 Pl 11 , 11 Р2 11 , 11 РЗ 11 , 11 РЗ 11 , : IRepository Products { get; } = new Product [ ] }, Price = 275М Price = 48. 95М } , Price = 19. SOM } , Price = 34 . 95М } } ; puЬlic void AddProduct {Product р) { / / Ничего не делать - для теста не требуется [Fact] puЫic void IndexActionModelisCornplete() //Организация var controller = new HorneController(); controller.Repository = new ModelCoшpleteFakeRepository(); 11 Действие var rnodel = (controller.Index() as ViewResult)?.ViewData.Model as IEnurneraЫe<Product>; 192 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 11 Утверждение Assert.Equal(controller.Repository.Products, шodel, Comparer.Get<Product>( (pl, р2) => pl.Name == p2.Name && pl. Price == р2. Price)); Мы определили фиктивную реализацию интерфейса IReposi tory, в которой при­ сутствует только свойство, необходимое для теста. и всегда применяются согласован­ ные данные (чего может не быть при работе с реальной базой данных. особенно в случае ее совместного использования с другими разработчиками, вносящими собс­ твенные изменения). Исправленный модульный тест по-прежнему не проходит. указывая на то, что при­ чиной проблемы является метод действия Index () в классе HomeController, а не компоненты, от которых он зависит. Метод действия. вызываемый модульным тес­ том, в достаточной степени прост для того, чтобы проблема стала очевидной в ре­ зультате его осмотра: puЫic IActionResult Index() => View(Repository.Products.Where(p => p.Price < 50)); Проблема связана с применением метода ся для фильтрации объектов Product Where () из LINQ, со значением свойства который использует­ Price, равным 50 или больше. Здесь уже есть серьезное указание на причину проблемы, но передовой опыт предполагает создание теста, который подтвердит наличие проблемы, прежде чем вносить корректирующую правку (листинг 7.13). Совет. В показанных тестах присутствует много дублированного кода. В следующем разделе объясняется, как упростить тесты. Листинг 7.13. Добавление теста в файле HomeControllerTests.cs из папки using using using using using WorkingWi thVisualStudio. Tests Microsoft.AspNetCore.Mvc; System.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xunit; namespace WorkingWithVisualStudio.Tests puЫic class HomeControllerTests ( class ModelCompleteFakeRepository : IRepository Products { get; 1 = new Product[] { "Pl", Price 275М ), "Р2", Price 48.95М }, "РЗ", Price 19.SOM ), "РЗ", Price 34.95М} puЫic IEnumeraЫe<Product> new new new new ); Product Product Product Product ( { { { Name Name Name Name Глава 7. Модульное тестирование приложений MVC puЫic 11 void AddProduct(Product р) { - для теста не Ничего не делать 193 требуется [Fact] void IndexActionModelisComplete() 11 Организация var controller = new HomeController{); controller.Repository = new ModelCompleteFakeRepository(); puЬlic 11 Действие var model = (controller.Index() as ViewResult)?.ViewData.Model as IEnumeraЬle<Product>; 11 Утверждение Assert.Equal(controller.Repository.Products, model, Comparer.Get<Product>( (pl, р2) => pl.Name == p2.Name && pl.Price == p2.Price)); class МodelCompleteFakeRepositoryPricesUnderSO puЫic IEnumeraЫe<Product> new new new new Product Product Product Product { { { { Name Name Name Name = = = = 11 Pl 11 11 Р2 11 , 11 РЗ 11 , 11 РЗ 11 , , : IRepository { Products { get; } = new Product [] Price = SM } , Price = 48. 95М } , Price = 19. SOM } , Price = 34 . 95М } } ; puЬlic void AddProduct (Product р) { / / Ничего не делать - для теста не требуется [Fact] puЬlic void IndexActionМodelisCompletePricesUnderSO() / / Организация var controller = new HomeController(); controller.Repository = new МodelCompleteFakeRepositoryPricesUnderSO(); //Действие var model = (controller.Index() as ViewResult)?.ViewData.Model as IEnumeraЬle<Product>; / / Утвер-.цение Assert.Equal(controller.Repository.Products, model, Comparer. Get<Product> ( (pl, р2) => pl . Name = р2 . Name && pl. Price = р2. Price) ) ; Мы определили фиктивное хранилище, которое содержит только объекты со значениями свойства Price меньше 50, Product и применили его в новом тесте. Запустив тест, можно заметить, что он проходит, подкрепляя идею о том, что проблема связана с применением метода Where () в методе действия Index () . 194 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 В реальном проекте выяснение причины отказа теста означает необходимость со­ гласования цели теста со спецификацией для приложения. Вполне может оказаться. что метод Index () обязан фильтровать объекты Product по свойству Price, в случае чего тест нуждается в пересмотре. Это распространенный итог, и тест, который не прошел, вовсе не всегда указывает на присутствие реальной проблемы в приложении. С другой стороны, если метод действия Index () не должен фильтровать объекты мо­ дели, тогда потребуется внести корректирующую правку (листинг 7.14). Понятие разработки через тестирование В настоящей главе я придерживаюсь наиболее часто используемого стиля модульного тес­ тирования, при котором мы пишем функцию приложения и затем тестируем ее, чтобы удос­ товериться в том, что она работает требуемым образом. Такой стиль популярен, поскольку большинство разработчиков думают сначала о прикладном коде, а затем о его тестирова­ нии (я определенно подпадаю под эту категорию). Проблема такого подхода в том, что он склоняет к созданию модульных тестов только для того прикладного кода, который было трудно писать или сложно отлаживать, оставляя ряд ас­ пектов каких-то функций лишь частично протестированными или вообще без тестирования. Альтернативным Developmeпt подходом - TDD). является разработка Существует много вариаций через TDD, тестирование (Test-Driveп но основная идея в том, что тесты для функции пишутся до того, как она реализуется. Написание тестов первыми заставля­ ет более тщательно думать о реализуемой спецификации и о том, как узнать, что функция была реализована корректно. Вместо погружения в детали реализации подход TDD застав­ ляет заранее продумывать, чем будет измеряться успех или неудача. Все создаваемые тесты изначально не будут проходить, т.к. новая функция еще не реали­ зована. Однако по мере добавления кода в приложение тесты постепенно будут переходить из красного состояния в зеленое, и ко времени завершения функции все тесты окажутся пройденными. Подход TDD требует дисциплины, но приводит к получению более полного набора тестов и может дать в результате более надежный и устойчивый код. Листинг 7.14. Удаление фильтрации посредством из папки LINQ в файле HomeController. cs Controllers using Microsoft.AspNetCore.Mvc; using WorkingWithVisualStudio.Models; using System.Linq; namespace WorkingWithVisualStudio.Controllers puЫic class HomeController : Controller ( puЫic IRepository Repository = SimpleRepository.SharedRepository; puЫic IActionResul t Index () => View (Reposi tory. Products) ; [HttpGet) puЫic IActionResult AddProduct() => View(new Product()); [HttpPost) IActionResult AddProduct(Product Repository.AddProduct(p); return RedirectToAction("Index"); puЫic р) { Глава 7. Модульное тестирование приложений MVC Запустив тесты снова. легко заметить. что все они проходят (рис. Тest С:-11> 1~: "S Run All 1 Run". ... 1 Playlist : All Tests • .А 7.6) . .... !:! Explorer 195 х Search Passed Tests (4) t) \.Yorking~/ithVisualStudio Tests.Hon eCon rollerTes s.lndexActio11Modell". 41 ms t) WorkingWithVisualStudio.Tests.Hon eControllerTes s.lndexActio11Modells". 2 ms t) \NorkingWithVisualStudio.Tests.ProductTбts.CanChangeProduc ame 1 ms & Norking~/ithVisualStudio Tests.Prod с Tests CanChangeProductPrice 12 ms Su пнnary Last Test Run Passed (Тotal Run Time ~00:0 5590048) t.) 4 Tests Oassed Рис. 7.6. Прохождение всех тестов Может показаться, что мы проделали слишком много работы для устранения та­ кой простой проблемы, однако возможность протестировать отдельный компонент жизненно важна при построении реального приложения. Достичь точки. где пробле­ ма идентифицирована и написаны тесты для проверки исправления , удастся лишь тогда. когда вы способны эффективно изолировать компоненты. Улучшение модульных тестов В предыдущем разделе был представлен базовый подход к написанию модульных тестов и прогону тестов в Visual Studio, а также акцентировано внимание на важ­ ности изоляции тестируемого компонента. В этом разделе рассматриваются расши­ ренные инструменты и средства. которые можно применять для написания тестов более согласованным и выразительным образом. Если вы сильно увлечетесь модуль­ ным тестированием, то можете в итоге получить настолько много тестового кода. что его ясность становится очень важной , особенно с учетом необходимости пересмотра тестов для отражения изменений в приложении, вносимых во время разработки и сопровождения. Параметризация модульного теста Тесты, написанные для класса HorneCon tro ll e r. выявили проблему . которая при ­ сутствовала только для определенных значений данных. Чтобы проверить такое ус­ ловие, были созданы два похожих теста. каждый из которых содержал собственное фиктивное хранилище. При таком подходе появляется дублированный код. тем бо­ лее с учетом того, что единственное отличие между двумя тестами связано с набором значений de c irna l, которые указываются для свойства фиктивных хранилищах . Pr i c e объектов Produ c t в Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 196 Инфраструктура xUnlt.net предлагает поддержку параметризованных тестов. когда данные, используемые в тесте. из него удаляются. так что единственный ме­ тод может применяться для множества тестов. В листинге 7. 15 с помощью средс­ тва параметризованных тестов устраняется дублированный код в тестах для класса HomeCon troller. Листинг 7.15. Параметриэация модульного теста в файле HomeControllerTests.cs из папки using using using using us ing WorkingWi thVisualStudio. Tests Microsoft.AspNetCore.Mvc; System.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xuni t; namespace WorkingWithVisualStudio.Tests puЫic class HomeControllerTests ( class ModelCompleteFakeRepository : IRepository puЬlic 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Ьlic void IndexActionМodelisComplete(decimal pricel, decimal price2, decimal priceЗ, decimal price4) 11 Организация var controller = new HomeController(); controller.Repository = newModelCompleteFakeRepository Products = new Product [] { new Product {Name = "Pl", Price = pricel } ' new Product {Name "Р2", Price = price2 }' new Product {Name "РЗ", Price = priceЗ } ' new Product {Name "Р4", Price = price4 }' = = = } ; 11 Действие var model (controller.Index{) as ViewResult)?.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)); Глава 7. Модульное те стирование nриложений MVC Параметризованные модульные тесты помечаются атрибутом рибута вместо ат­ который используется для стандартных тестов . Здесь также применя­ Fact, ется атрибут Th eory 197 InlineDat.a, который позволяет указывать значения для аргументов, определяемых методом модульного теста. Язык С# ограничивает способ выражения значений данных в атрибутах, поэтому мы определили четыре аргумента тестового метода и посредством атрибута Inline Data decimal предоставили для них значе­ ния. Значения тестового метода используются для генерации масси ­ ва объектов применяется для установки свойства de c i mal внутри Pr o du c t, который Produ cts объ­ екта фиктивного хранилища. Каждый атрибут InlineData определяет отдельный модульный тест, который по­ казан как индивидуальный элемент в окне Запись в окне Test Explorer Test Explorer среды Visual Studlo (рис . 7. 7). отображает значения . которые будут использоваться для аргументов метода модульного теста. • !:! х Р - Rul'IAIJ 1 Run... • 1 P.•yt.U:All'Тt1b • Summary ~ Pas.иd Tкts (<1} $ & WoНcfn9\Vid\Vi1ualStWio.Т..1.1.HorмCOF1VolltrTeJts.lnc!.xAФonModt!ll~p~eo(ptic•11275, рпсе2: 48,95, pt1(.e): 10.S, pr.c.4: lol.95) WorЩWith\li1ualStudio.Tests.Нome<:ontroll~T1tsts.ll'ldtxActюnМode!l,Comp~pric:elr S. priu2: 48.95, priceЗ: 19.S. pr1e~: 24.9S) -40 rns 1 ms ~ Workint;W1thl/iJu•!Studio.1'e-sts.ProductТtotts.C.neh&~ProductNaiм 1 ms f) Worktng\V'ltt\V11ua1Studic.1tsts.Ptoduuit1ts.UinCNi1'941PтodLКWnte 11 rns Рис. 7.7. Параметризованные тесты в окне ' t..ast Tttt Run P•sstd (Тot.11 R.un Ttmt O:OQ.i)t, t) 4 т~s Piutd Test Explorer среды Visual Studio Получение тестовых даннь1х из метода или свойства Ограничения . налагаемые на выражение данных в атрибутах, сокращают по­ лезность атрибута Inl i n e Da ta . Альтернативный подход предусматривает создани е статического метода или свойства, возвращающего объект, который требуется для тестирования. В такой ситуации нет никаких ограничений относительно того , как определяются данные, и можно создавать более широкий диапазон тестовых значе­ ний . Чтобы продемонстрировать подход в работе, добавим в проект модульного тес­ тирования файл класса по имени в листинге Листинг Produc t TestDa ta . cs с определением . показанным 7.16. 7 .16. Содержимое файла ProductTes tDa ta. cs WorkingWithVisualStudio.Tests из папки us ing System. Collec ti on s; us ing System. Collecti o ns.Gene ric; us ing Wo rkingWith Vi sual St ud io . Mode l s ; name s p ace Wor ki n gW i thV i sualSt udio . Tes t s p u Ы i c class Pr oductTestData : IEn u meraЬle < object[]> puЫic IEn umerator<object[ ] > GetEnumerator() { yield retur n new obje c t[J { Ge t Pr ic es UnderSO() ); yield re t ur n n ew objec t[ ] { GetPricesOverS O }; I Enumera t or IEnu mera Ы e.Ge t Enumerator() return this . GetEnumerator() ; Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 198 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 [] GetPricesOverSO => new Product[J 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<object []>.возвращающий последовательность массивов объектов. Каждый массив объектов в последовательности содержит один набор аргументов. ко­ торые будут передаваться тестовому методу. Мы переопределим тестовый метод. что­ бы он принимал массив объектов один уровень - Product. который добавит к тестовым данным еще перечисление массивов объектов Product. Такая глубина структуры тестовых данных может сбивать с толку. но важно понимать, что тесты не будут ра­ ботать, если количество аргументов, которые xUnit.net пытается передать тестовому методу, не соответствует сигнатуре метода. Мне нравится структурировать классы тестовых данных так. чтобы закрытые ме­ тоды или свойства определяли индивидуальные наборы тестовых данных, которые затем с помощью метода GetEnumerator () объединялись в последовательности мас­ сивов объектов. Для иллюстрации различных приемов массивы объектов Product были созданы с применением и метода. и свойства. но я предпочитаю использовать в своих проектах один подход (на его выбор влияет вид данных, с которыми проводится тестирование). В листинге ньхх с атрибутом Theory 7.17 показано, как можно применять класс тестовых дан­ для настройки тестов. Совет. Если вы хотите включить тестовые данные в тот же самый класс, который определяет модульные тесты, тогда можете использовать атрибут MemЬerData вместо Атрибут MemberData ClassData. конфигурируется с применением строки, указывающей имя стати­ ческого метода, который будет предоставлять реализацию I En ume r аЫ е< оЬ j ее t [ ] >, где каждый массив объектов в последовательности является набором аргументов для тес­ тового метода. Листинг 7. 17. Использование класса тестовых данных в файле HomeControllerTests. cs using using using using using из папки WorkingWi thVisualStudio. Tests Microsoft.AspNetCore.Mvc; System.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xunit; Глава 7. Модульное тестирование приложений MVC 199 namespace WorkingWithVisualStudio.Tests { puЫic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЬlic IEnumeraЫe<Product> puЫic 11 Products { get; set; void AddProduct(Product Ничего не делать - { р) для теста не требуется (Theory) [ClassData(typeof(ProductTestData))] puЫic void IndexActionМodelisComplete(Product[] products) { 11 Организация var controller = new HomeController(); controller.Repository = new ModelCompleteFakeRepository Products = products }; 11 Действие (controller.Index() as ViewResult)?.ViewData.Model var 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 конфигурируется с типом класса тестовых данных, которым в рассматриваемом случае является инфраструктура xUnit.net ProductTestData. Во время выполнения тестов создаст новый экземпляр класса ProductTestData, пос­ ле чего будет применять его для получения последовательности тестовых данных для теста. На заметку! Если вы посмотрите на список тестов в окне IndexActionModelisComplete () ProductTestData предоставляет два Test Explorer, то увидите, что для тестов предусмотрена единственная запись, хотя класс набора тестовых данных. Так происходит в ситуации, когда объекты тестовых данных не удается сериализировать, и проблему можно решить, применив к тестовым объектам атрибут SerializaЫe. Улучшение фиктивных реализаций Эффективная изоляция компонентов требует фиктивных реализаций классов, что­ бы предоставить тестовые данные или проверить, ведет ли себя компонент так, как должен. В предшествующих примерах создавался класс, реализующий интерфейс IReposi tory. Такой подход может быть эффективным, но он приводит к созданию классов реализаций для каждого вида теста, который желательно прогнать. В качес­ тве примера в листинге демонстрируется добавление теста, который проверяет, что метод действия вызывает метод 7.18 Index () Products () в хранилище только один 200 Часть 1. Введение в инфраструктуру ASP.NEТ Соге MVC 2 раз. (Тест подобного вида распространен, когда имеется опасение, что компонент де­ лает дублирующиеся запросы к хранилищу, становясь причиной множества запросов к базе данных.) Листинг 7 .18. Добавление модульного теста в файле из папки using using using using using using HomeControllerTests. cs WorkingWi thVisualStudio. Tests Microsoft.AspNetCore.Mvc; System.Collections.Generic; WorkingWithVisualStudio.Controllers; WorkingWithVisualStudio.Models; Xunit; System; namespace WorkingWithVisualStudio.Tests puЫic class HomeControllerTests { class ModelCompleteFakeRepository : IRepository puЬlic IEnumeraЬle<Product> puЫic 11 Products ( get; set; void AddProduct(Product р) { - для теста не Ничего не делать требуется [Theory] [ClassData(typeof(ProductTestData))] puЬlic void IndexActionModelisComplete(Product[] products ) { 11 Организация var controller = new HomeController(); controller.Repository = new ModelCompleteFakeRepository Products = products }; 11 Действие var model (controller.Index() as ViewResult)?.ViewData.Model as 11 IEnumeraЬle<Product>; Утверждение Assert.Equal(controller.Repository.Products, model, Comparer. Get<Product> ( (pl, р2) => pl. Name == р2. Name && pl.Price == p2.Price)); class PropertyOnceFakeRepository : IRepository puЫic int PropertyCounter { get; set; } =О; puЬlic IEnumeraЫe<Product> Products get { PropertyCounter++; return new [] { new Product { Name = "Pl" , Price puЬlic void AddProduct (Product р} / / Ничего не депать - для теста не требуется = 100 } }; Глава 7. Модульное тестирование приложений MVC 201 [Fact] puЫic void RepositoryPropertyCalledOnce() //Организация var repo = new PropertyOnceFakeRepository(); var controller = new HomeController ( Reposi tory = repo }; //Действие var resul t = controller. Index () ; / / Утверждение Assert.Equal(l, repo.PropertyCounter); } Фиктивные реализации - не всегда простые источники данных; они также могут использоваться для оценки способа, которым компоненты выполняют свою работу. В этом случае было добавлено простое свойство счетчика, которое увеличивается каждый раз. когда читается свойство метода Assert. Equal () Products фиктивного хранилища. и с помощью выполнена проверка, что обращение к свойству производи­ лось только один раз. Добавление инфраструктуры имитации Создание фиктивных объектов такого рода выходит из-под контроля. и лучший способ вернуть себе контроль - применить инфраструктуру фиктивных объектов, также называемую инфраструктурой имитац~щ. (Существует формальная разница между фиктивными и имитированными объектами, но для облегчения использования современные инструменты тестирования размывают ее, а потому данные термины будут применяться взаимозаменяемо.) В главе используется инфраструктура торая описана в табл. Таблица 7 .4. Moq, ко­ 7.4. Помещение Moq Вопрос Ответ Что это такое? Moq - в контекст зто программный пакет для создания фиктивных реализа­ ций компонентов в приложении Чем она полезна? Инфраструктура имитации облегчает создание фиктивных ком­ понентов с целью изоляции частей приложения для модульного тестирования применяет лямбда-выражения для определения функциональ­ Как она Moq используется? ности фиктивных компонентов и требует определения только тех средств, которые используются при тестировании Существуют ли Привыкание к синтаксису может потребовать некоторых усилий. какие-то скры­ Документация и примеры находятся по адресу тые ловушки или github.com/Moq/moq4 h t tps : / / ограничения? Существуют ли Доступно несколько альтернативных инфраструктур, в том числе альтернативы? NSubstitute (https: / /nsubstitute. github. io) и FakeltEasy (https: / /fakeiteasy. github. io/). Все они предлагают похо­ жие возможности, и выбор между ними связан только с предпочи­ таемым вами синтаксисом Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 202 Чтобы установить Moq. щелкнем правой кнопк о й мыши на проекте Wo r kingW i thVisualS t udio. Tes t s в окне Solution Explorer и выберем в контекстном меню пункт Manage NuGet Packages for Solutioп (Управление пакетами NuGet длл ре­ шенил). Перейдем на вкладку Browse (Обзор) и введем Moq в поле поиска. Выберем Mo q из списка пакетов (рис. 7.8) и щелкнем на кнопке lпstall (Установить) длл добав­ ленил пакета в проект. На заметку! Пакет Moq добавляется в проект модульного тестирования , но не в проект приложения. Browse lnstalled UpdatesO NuGet Package Manager: WorkingWithVisualStudio.Tests . moq о oq Moq Ьу 0.11nitl C1uulino, lau, 14.2М downloads Moq is the most popular 1nd frie.лdly mocking fr1mework (ог .NЕТ ~ v4.7.99 Version: [Latest st1Ьle4.7.9!: " Autofac.Extras.Moq Ьу Autofac.Ьtr1s.Moq, 124К downlo1d' L lnst1ll v4.1.0·rc5-246 The Moq integrltion posck19t 1llows you to 1utom1tically create mock both concrete 1nd mock. abstract insti!lncts 1n unit tбts". P.tt•••и de:pender'Юes for Ш1ШJ Moq.Contrib by kzu.nц 71.бKdownlo•ds v0.3.0 Grace.Moq Ьу lan Johnsol\ 8.34К downloads Oescription Moq ts the most popular and fr1endly mock1ng fr•mework for . NЕТ Contributed features and third-party integr.tion for Moq. v2A.2 Gra<t.Moq is • comp1nion libr•ry for Gract 1nd Moq Version: 4.7.99 Author(s): D1nitl C1zzulino, lczu Lkeмei Mtps:// r•\~. itlшbustrcontent с Рис. 7 .В. Добавление пакета в проект модульного тестирования После установки пакета тами m Moq средой Visual Studio закроем окно управления паке ­ NuGet. Соэдание имитированного объекта Создание имитированн о го объекта о зн ач ает н е обходимост ь со об щенил Moq о том, какого вида объект интересует. конфигурирование его поведенил и применени е этого объекта к тестируемой сущности. В листинге 7. 19 инфраструкту ра Moq ис­ пользуетсл длл зам е ны двух фиктивных хранилищ в тестах, относящихсл к классу HomeContro ller. Листинг us ing us ing us ing using us in g ti si ng 7.19. Применение имитированных объектов в файле HomeControllerTests. св из папки WorkingWi thVisualStudio. Tests Micros o ft. As pNetC ore.Mvc; System.Coll e ctions.Gener i c ; WorkingWi t hVi sualStudi o.Contro lle r s; WorkingWi t hVi s ua l Studi o.Mode l s ; Xunit; System; Глава 7. Модульное тестирование приложений MVC 203 using Moq; namespace WorkingWithVisualStudio.Tests puЫic class HomeControllerTests { [Theory] [ClassData(typeof(ProductTestData))] puЫic void IndexActionModelisComplete(Product[] products ) { //Организация var mock = new Мock<IRepository>(); mock.SetupGet(m => m.Products) .Returns(products); var controller = new HomeController ( Repository = mock.Object } ; //Действие 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 == p2.Name && pl. Price == р2. Price)); [Fact] void RepositoryPropertyCalledOnce() 11 Организация var mock = new Мock<IRepository>(); mock. SetupGet (m => m. Products) .Returns(new[] { new Product ( Name = "Pl", Price = 100 } }) ; var controller = new HomeController { Repository = mock.Object } ; puЬlic //Действие var result = controller.Index(); 11 Утверждение mock.VerifyGet(m => m.Products, Times.Once); } Использование IReposi tory Moq позволило удалить фиктивные реализации интерфейса и заменить их всего лишь несколькими строками кода. Здесь не приво­ дятся детальные сведения о разных поддерживаемых объясняется способ применения адресу Moq. https: / /github. com/Moq/moq4. различных типов компонентов MVC Moq средствах, но на примерах Примеры и документация по Moq доступны по (При обсуждении модульного тестирования в оставшихся главах книги также будут приво­ диться примеры.) Первым делом создается новый объект Mock с указанием интерфейса, который должен быть реализован: var mock = new Мock<IRepository>(); Созданный объект Mock будет имитировать интерфейс IReposi tory. Далее оп­ ределяется функциональность, которая требуется для теста. В отличие от обычной реализации интерфейса классом для имитированного объекта конфигурируется толь­ ко поведение. нужное для теста. В первом имитированном хранилище понадобится 204 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 реализовать свойство Products, чтобы оно возвращало набор объектов торый передается тестовому методу через атрибут Product, ко­ ClassData: mock.SetupGet(m => m.Products) .Returns(products); Метод SetupGet () используется при реализации средства извлечения для свойс­ тва. Аргументом этого метода является лямбда-выражение, которое указывает подле­ жащее реализации свойство (Products в данном примере). Метод вается на результате, полученном из метода SetupGet (), Returns () вызы­ чтобы указать результат. который будет возвращаться, когда читается значение свойства. Для второго имити­ рованного хранилища применяется тот же самый подход, но указывается фиксиро­ ванное значение: mock. SetupGet (m => m. Products) .Returns (new[] { new Product { Name В классе Mock определено свойство Obj ect, = "Pl", Price = 100 } }) ; возвращающее объект, который ре­ ализует указанный интерфейс и обладает ранее определенным поведением. В обоих модульных тестах свойство Obj ect используется, чтобы получить хранилище для конфигурирования контроллера: var controller = new HomeController { Repository = mock.Object }; Последним примененным средством Products Moq была проверка того. что к свойству осуществлялось только одно обращение: mock.VerifyGet(m => m.Products, Times.Once}; Метод VerifyGet () относится к тем методам класса Mock, которые инспектиру­ ют состояние имитированного объекта, когда тест завершен. В данном случае ме­ тод VerifyGet () позволяет проверить, сколько раз читалось свойство Products. Times. Once указывает. что метод VerifyGet () должен генерировать ис­ Значение ключение, если свойство читалось не в точности один раз, и это приведет к тому. что тест не пройдет. (Методы класса Assert, обычно используемые в тестах, генерируют исключение, когда тест не проходит, и потому при работе с имитированными объек­ тами метод VerifyGet () можно применять для замены метода класса Assert.) Резюме rлава была сконцентрирована на модульном тестировании, которое способ­ но оказаться мощным инструментом, направленным на повышение качества кода. Модульное тестирование не подойдет абсолютно каждому разработчику, но с ним по­ лезно поэкспериментировать, и оно может быть полезным, даже если используется только для сложных функций или обнаружения проблем. Было описано применение инфраструктуры тестирования xUnit.net, объяснена важность изоляции компонентов для целей тестирования и продемонстрированы некоторые инструменты и приемы, позволяющие упростить код модульных тестов. В следующей главе начнется разра­ ботка более реалистичного приложения MVC под названием SportsStore. ГЛАВА 8 SportsStore: реальное приложение главах мы создавали очень простое приложение MVC. Был в предшествующихMVC, описан nаттерн основные средства языка С#, а также инструменты, не­ обходимые профессиональным разработчикам приложений MVC. Наступило время собрать все вместе и построить несложное, но реалистичное приложение электрон­ ной коммерции. Наше приложение под названием SportsStore будет следовать классическому под­ ходу, который повсеместно используется в онлайновых магазинах. Мы создадим он­ лайновый каталог товаров, который потребители смогут просматривать по категори­ ям и страницам, корзину для покупок, куда пользователи смогут добавлять и удалять товары, и форму оплаты, где потребители смогут вводить сведения, связанные с до­ ставкой. Кроме того, мы создадим административную область, которая включает в себя средства создания, чтения, обновления и удаления CRUD) (create, read, update. delete - для управления каталогом товаров. и защитим ее так. чтобы изменения могли вносить только зарегистрированные администраторы. Цель настоящей и последующих глав хожа реальная разработка приложений - дать вам возможность увидеть, на что по­ MVC, за счет создания примера приложения, который максимально приближен к реальности. Разумеется, мы будем ориентиро­ ваться на ASP.NET Core MVC, поэтому интеграция с внешними системами. такими как база данных, предельно упрощена. а определенные части приложения, например, обработка платежей, вообще отброшены. Построение всех уровней необходимой инфраструктуры может показаться не­ сколько медленным, но первоначальные трудозатраты при разработке приложения MVC окупаются, обеспечивая удобный в сопровождении, расширяемый и хорошо структурированный код с великолепной поддержкой модульного тестирования. Модульное тестирование Я уже достаточно много говорил о легкости проведения модульного тестирования в MVC, а также о том, что модульное тестирование может быть важной и полезной частью процесса разработки. Это будет демонстрироваться на протяжении данной части книги, поскольку я описываю нюансы и приемы модульного тестирования в их взаимосвязи с основными средствами MVC. 206 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Я понимаю, что мое мнение не является единственно верным. Если вы не хотите прибе­ гать к модульному тестированию, то меня это вполне устроит. Таким образом, когда что-то относится исключительно к тестированию, оно будет помещаться во врезку, подобную на­ стоящей. Если модульное тестирование вас не интересует, тогда можете смело пропускать такие врезки, и приложение SportsStore будет раба.тать не менее успешно. Чтобы восполь­ ASP.NET Core MVC, вовсе не обязательно проводить зоваться преимуществами технологии какое-либо модульное тестирование, хотя поддержка тестирования, конечно же, является основной причиной перехода на ASP.NET Core MVC. Большинству средств МVС, используемых в приложении SportsStore. посвящены отдельные главы далее в книге. Вместо того чтобы повторять весь материал, я сооб­ щаю ровно столько, сколько требуется для понимания этого примера приложения, и указываю главу. в которой можно почерпнуть более подробные сведения. Каждый шаг, необходимый для построения приложения. будет выделен, что поз­ волит понять, каким образом средства MVC сочетаются друг с другом. Вы должны уделить особое внимание созданию представлений. Если вы не будете в точности сле­ довать примерам, то получите несколько странные результаты. Начало работы Если вы планируете писать код приложения SportsStore на своем компьютере во время изучения материала текущей части книги, тогда вам придется установить Visual Studio и удостовериться в том, что установлен вариант LocaIDB. который тре­ LocalDB устанавливается автома­ главы 2. буется для постоянного хранения данных. Вариант тически в случае следования инструкциям из На заметку! Если вы просто хотите работать с проектом, не воссоздавая его, то можете за­ грузить готовый проект SportsStore из хранилища GitHub для настоящей книги (https: / / github.com/apress/pro-asp.net-core-mvc-2). Разумеется, вы вовсе не обяза­ ны повторять все действия. Я старался делать снимки экрана и листинги кода максималь· но простыми в отслеживании на тот случай, если вы читаете книгу в поезде, кафе или где-то еще. Создание проекта MVC Мы будем следовать тому же самому базовому подходу, который использовался в предшествующих главах и заключается в том, чтобы начать с пустого проекта и до­ бавлять в него все необходимые конфигурационные файлы и компоненты. Выберем в меню File (Файл) среды Visual Studio пункт New~Project (Создать~Проект) и ука­ ASP.NET Core Web Applicatioп (Веб-приложение ASP.NET Core). рис. 8.1. Установим имя проекта в SportsStore и щелкнем на жем шаблон проекта как показано на кнопке ОК. Выберем шаблон Empty (Пустой). как проиллюстрировано на рис. 8.2. Прежде чем SportsStore, удостоверимся в том, что в списках в верхней части окна выбраны варианты .NET Core и ASP.NET Core 2.0, а флажок ЕnаЫе Docker Support (Включить поддержку Docker) не отмечен. щелкать на кнопке ОК для создания проекта Глава 8. SportsStore: реальное прил оже н ие 207 L А lnstalled ~ R:,V Vj] Vj] " Visual (;t Windows Classic Desktop Web .NЕТ Core .NЕТ Standard Cloud Test ~ Visual Basic Console Арр (.NET Core) Visual С# Class Library (.NЕТ Core) Visual (# Unit Test Project (.NЕТ Core) Visual (:t xUnit Test Project (.NЕТ Core) Visual С# ~ .ASP.NEТ Cor~ Web Applicat1on . Visual (# ~()1 ~•rvPr Not finding what you are looking for? Open Visual Studio lnstaller Name: SportsStore Location: ·! /C:\Users\ adam\Documents\ Solution name: Browse". 0 SportsStore 0 Рис. . j .NEТCore 1 ReactJs ~ WebAPI ASP.NEТ ? . . Core 2.0 @:] l'leb Application Выбор типа проекта 8. 1. N...., ASP NЕТ Core l'leb Applocot•on • Spolt<Store Createdi дddtoS Х ~ @.1 е Web Angular An empty project template for creating an ASP. NEТ Cor!! application. This template does not have any content in it. AppliclJtion (Model-ViewController) @:> ReactJs and Redux Ch~ngt Avthentication Authentication No Authentication О Еn•Ы• Docker Support OS; \V.ndows Rtquir6 Oocker forWindows Oocker support сап also Ье enaЫed later ~ ОК Рис. 8.2. Выбор шаблона проекта \ J Cane<I 208 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Создание структуры папок Следующий шаг заключается в добавлении папок, которые будут содержать ком­ поненты, требуемые для приложения MVC: модели. контроллеры и представления. 8.1, щелкнем правой кнопкой мыши на эле­ Solutioп Explorer, выберем в контекстном меню Чтобы создать папки, описанные в табл. менте проекта SportsStore в окне Folder (Добавить~Новая пункт Add~New папка) и введем имя папки. Позже потребу­ ются дополнительные папки, но создаваемые сейчас папки отражают главные части приложения MVC Таблица Папки, требующиеся для проекта 8.1. и для начала их вполне достаточно. SportsStore Имя Описание Models Эта папка будет содержать классы моделей Controllers Эта папка будет содержать классы контроллеров Views Эта папка будет содержать все, что относится к представлениям, в том числе индивидуальные файлы Razor, файл запуска представ­ ления и файл импортирования представлений Конфмrурирование приложения За конфигурирование приложения ASP.NEТ Core несет ответственность класс В листинге 8.1 S tart up. Startup для включения инфра­ приведены изменения, внесенные в класс стрУК1УJ>Ы МVС и нескольких связанных средств, которые полезны при разработке. На заметку! Класс Startup является важным функциональным средством будет подробно описан в главе Листинг 8.1. ASP.NET Core и 14. Включение средств в файле Startup. cs из папки Sportsstore using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Dependencyinjection; namespace SportsStore { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddНvc(); ) puЬlic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); арр. UseМvc (routes => { }) ; ) арр, IHostingEnvironment env) { Глава 8. SportsStore: реальное nриложение Метод ConfigureServices () 209 применяется для настройки разделяемых объектов, которые могут использоваться повсеместно в приложении через средство внедрения зависимостей, которое рассматривается в главе внутри ConfigureServices (), 18. Метод AddMvc (), вызываемый является расширяющим методом, который настраи­ вает разделяемые объекты, применяемые в приложении МVС. Метод используется для настройки средств, которые получают и Configure () обрабатывают НТТР-запросы. Каждый метод, вызываемый в методе Configure (), представляет собой расширяющий метод, который настраивает средство обработки НТТР-запросов (табл. 8.2). Таблица 8.2. Методы настройки первоначальных средств, вызываемые в классе Startup Метод Описание UseDeveloperExceptionPage () Этот расширяющий метод отображает детали исключения, которое произошло в приложении, что полезно во время процесса разработки. Он не должен быть включен в раз­ вернутых приложениях и при развертывании приложения в главе 12 будет показано, как отключить данное средство Этот расширяющий метод добавляет простое сообщение UseStatusCodePages() в НТТР-ответы, которые иначе не имели бы тела, такие как ответы UseStaticFiles() 404 - Not Found (404 - не найдено) Этот расширяющий метод включает по,одержку для обслу­ wwwroot живания статического содержимого из папки UseMvc () Этот расширяющий метод включает инфраструктуру ASP.NEТ Core MVC Далее понадобится подготовить приложение для представлений правой кнопкой мыши на папке ltem Views, (Добавить9Новый элемент) и укажем шаблон импортирования представлений МVС) из категории Щелкнем на кнопке Add MVC View lmports Page ASP.NET (рис. Add9New (Страница 8.3). (Добавить), чтобы создать файл_Viewimports. и приведем его содержимое в соответствие с листингом Листинг Щелкнем Razor. выберем в контекстном меню пункт cshtml, 8.2. 8.2. Содержимое файла _ Viewimports. cshtml из папки Views @using SportsStore.Models @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Оператор @using позволит применять типы из пространства имен SportsStore. Models в представлениях, не ссылаясь на это пространство имен. Оператор @addTagHelper включает встроенные вспомогательные функции дескрипторов, ко­ торые бу.цут использоваться позже для создания элементов НТМL, отражающих кон­ фигурацию приложения SportsStore. Создание проекта модульного тестирования Создание проекта модульного тестирования требует выполнения того же самого процесса, который бьm описан в главе решения SportsStore Add9New Project в окне Solutioп 7. Щелкнем правой кнопкой мыши Explorer и выберем в контекстном (Добавить9Новый проект). на элементе меню пункт Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 210 ,,, ASP.NEТ Core Code Gene:ral ~· MVC View P•g• ASP.NEТ ~· MVC View L•yout P•g• ASP.NEТ Со"' ~· МVС ~ MVC V1<w lmports Page ASP.NEТ Coro Rozor T•g Holpor ASP.NET Core Middlewor• Cl•ss ASP.NET Coro Startup cl•ss ASP.NEТ Wtb Configuration File ASP.NET Core о " Web ASP.NEТ General Scripts Contont Online ~ Nome: Viow Start Pago Coro ASP.NET Coro Core _Viowlmports.cshtml [ Рис. Укажем шаблон 8.3. Создание файла импортирования представлений xUnit Test Project (.NET Core) как показано на рис. Core)), 8.4, (Проект тестирования и установим имя проекта в xUnit (.NET Spo r tsStor e. Te s t s. Щелкнем на кнопке ОК, чтобы создать проект модульного тестирования. " lnstalled '1 ,,, Visual С# Windows Classic Desktop Web Standard Cloud Test Visual Basic SQL Sorver .NЕТ ~ Visua\CI! ~~ Class Library (.NET Core) Visual С" Vl5 Unit Тбt Project (.NET Core) Visual CI! ~ xUnit Тбt Pr~joct (.NЕТ Coro) VtSual (:: @ ASP.NET Coro Web Application VisualCI! [1~с• .NEТCcre ~ Console Арр (.NЕТ Core) А project can run о~ and МасО . Online Not finding what you are looking for? Open Visual Stud10 lnstaller №me: Locotion: SportsStore.Tбts !C:\Users\adam\Documents\SportsStore Рис. 8.4. Создание проекта модульного тестирования Browse". Глава 8. SportsStore: реальное приложение 211 После того. как проект модульного тестирования создан. щелкнем правой кнопкой Spo rtsStore. Tests в окне Solution Explorer и выберем в контекстном (Редактировать Sports Store. Test s. cspro j). Внесем в файл SportsStore. Tests. csproj изменения , показанные в листинге 8.3, чтобы добавить пакет Moq в проект Spo rtsStore. Tests и создать ссьшку на главный проект Spo rtsSt o re. Удостоверимся . что для пакета Moq указана та же версия , что и в листинге 8.3. мыши на проекте меню пункт Листинг Edit SportsStore.Тests.csproj 8.3. Добавление пакета в файле SportsStore. Tes ts. csproj из папки SportsStore. Tests <Pro ject Sdk= " Microsoft .NET. Sdk"> <PropertyGro up > <TargetFramework>netc o reapp 2 . 0</TargetFramework> < I sPa c kaЬle>false</ I sPackaЫ e> </ PropertyGr o up> <ItemGroup> <ProjectReference Include=" .. \SportsStore\SportsStore.csproj" /> </ItemGroup> < It e mGro up > <PackageReference I nclude= "M icros of t.NET.Test .Sdk" Ve rsion= " lS.3.0-preview - 20 1 70628- 0 2 " / > <P ackageReference In c lude= "x unit " Version=" 2.2 .0" /> <Pa ckageReference Include= " xunit .runner.vi suals t udio " Version= " 2 .2.0" /> <PackageReference Include="Moq" Version="4.7.99" /> </ ItemGr o up> </ Pr o jec t > Как только изменения будут сохра­ Visual Studio загрузит и ус­ пакет Moq в проект модульного нены, среда тановит тестирования и создаст ссылку на глав­ ный проект SportsStore, Solution Explorer Search $olulion &рlош (Ctrf+;) Р· так что содер­ жащиеся в нем классы смогут использо ­ " ваться в тестах. Проверка и запуск приложения Проекты приложения и модульного тестирования созданы, сконфигурирова­ ны и готовы к дальнейшей разработке. Окно ----- Solution Explorer должно содержать элементы, представленные на ри с . 8.5. Если вы видите другие элементы или элементы расположены не в тех позици ­ " ;;:] SportsStore 41 Conne<ted Services ~ .~.· Oependencies ~ }' Properties @wwwroot Controlfers Models Views " ~ В _Viewlmports.cshtml с• Program.cs ~ С" Startup.cs ~ 15З SportsStore.Tests .-:.• Oependencies ~ ~ Со UnitТest1 .cs ях. тогда возникнут проблемы. поэтому уделите время проверке. что все элемен­ ты присутствуют и местах. находятся на своих Рис. 8.5. Окно Solution Explorer для про­ SportsStore и модульного ектов приложения тестирования 212 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 В результате выбора в меню Debug (Отладка) пункта Start Debugging (Запустить Start Without Debugging (Запустить без отладки). если предпочтение от­ дается итеративному стилю разработки. описанному в главе 6, появится страница ошибки (рис . 8 .6). Сообщение об ошибке отображается из-за того, что в настоящий отладку) или момент в приложении нет контроллеров для обработки запросов; проблему мы вскоре устраним. Status Code: 404; Not Found Рис. 8.6. Выполнение приложения SportsStore Начало работы с моделью предметной области Все проекты начинаются с создания модели предметной области. которая являет­ ся центральной частью приложения MVC. Поскольку мы строим приложение элект­ ронной коммерции, то наиболее очевидная модель. которая необходима, относится к товару. Добавим в папку Models приведенным в листинге 8.4. Листинг 8.4. Содержимое файла файл класса по имени Product. cs из папки Product. cs с определением. Model s namespace Sp o rtsStore.Models { class Product { int Pr od uc t ID { get; set; p u Ьli c string Name ( get; set; } puЫ i c string Desc ri ption { get; se t; puЬlic d ec imal Price { get; set; puЫic str i ng Catego ry ( get; set; } puЫic puЬli c Создание хранилища Нам нужен какой-нибудь способ получения объектов Product из базы данных . В главе З упоминалось . что модель включает в себя логику для сохранения и извле<1ения данных из постоянного хранилища. Пока можно не беспокоиться о том , как будет реа­ лизовано постоянство данных. но необходимо начать процесс определения интерфей­ са для него. Добавим в папку Models новый файл по имени IProduc t Reposi t.ory. cs и поместим в него определение интерфейса. показанное в листинге 8.5. Глава 8. SportsStore: реальное приложение Листинг 8.5. Содержимое файла IProductReposi tory. cs из папки 213 Models using System.Linq; namespace SportsStore.Models puЫic interface IProductRepository Products { get; IQueryaЫe<Product> Интерфейс волить I ProductReposi tory вызывающему коду получать использует IQueryaЬle<T>, последовательность объектов чтобы поз­ Р r о d u с t. Интерфейс IQueryaЫe<T> является производным от более знакомого интерфейса IEnumeraЫe<T> и представляет коллекцию объектов, которые можно запрашивать, вроде тех, что управляются базой данных. Класс, зависящий от интерфейса ты Product IProductReposi tory, способен получать объек­ без необходимости знать детали того, как они хранятся, или каким обра­ зом класс реализации будет их доставлять. Интерфейсы IEnumeraЬle<T> и IQueryaЫe<T> Интерфейс IQueryaЬle<T> удобен, потому что он позволяет эффективно организо­ вать запрашивание коллекции объектов. Позже в главе будет добавлена поддержка для извлечения из базы данных подмножества объектов Product. Применение интерфейса IQueryaЫe<T> дает возможность запрашивать у базы данных именно те объекты, кото­ рые требуются, с помощью стандартных операторов LINQ, не нуждаясь в информации о том, какой сервер баз данных хранит данные или как он обрабатывает запрос. Без интерфейса IQueryaЫe<T> пришлось бы извлекать из базы данных все объекты Product и затем отбрасывать ненужные, что с ростом объема данных, используемых приложением, превра­ щается в затратную операцию. Именно по этой причине в интерфейсах и классах хранилища в форме базы данных обычно вместо IEnumeraЫe<T> применяется IQueryaЫe<T>. Однако во время использования интерфейса IQueryaЬle<T> следует соблюдать осторож­ ность, поскольку каждый раз, когда происходит перечисление коллекции объектов, запрос будет оцениваться заново, т.е. базе данных отправится новый запрос. В результате выго­ ды в плане эффективности от применения IQueryaЫe<T> могут быть сведены на нет. В таких ситуациях IQueryaЫe<T> можно преобразовать в более предсказуемую форму, используя расширяющий метод ToList () или ToArray (). Создание фиктивного хранилища Теперь, когда определен интерфейс, можно было бы реализовать механизм пос­ тоянства и привязать его к базе данных. но сначала необходимо добавить ряд дру­ гих частей приложения. Для этого мы создадим фиктивную реализацию интерфейса IProductRepository, которая будет замещать хранилище данных до тех пор, пока мы им не займемся. Чтобы создать фиктивное хранилище, добавим в папку файл класса по имени FakeProductReposi tory. cs представленное в листинге 8.6. Models и поместим в него определение, Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 214 Листинг 8.6. Содержимое файла FakeProductReposi tory. cs из папки Models using System.Collections.Generic; using System.Linq; namespace SportsStore.Models { puЫic class FakeProductRepository : IProductRepository { puЬlic IQueryaЫe<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 ) ).AsQueryaЬle<Product>(); Класс FakeProductReposi tory реализует интерфейс возвращая в качестве значения свойства ектов Product. Метод AsQueryaЬle () Products IProductReposi tory. фиксированную коллекцию объ­ применяется для преобразования фиксиро­ ванной коллекции объектов в IQueryaЫe<Product>. чтобы реализовать интерфейс IProductRepository и позволить создавать совместимое фиктивное хранилище. не имея дело с реальными запросами. Регистрация службы хранилища В МVС придается особое значение использованию слабо связанных компонентов, что означает возможность вносить изменения в одну часть приложения, не внося со­ ответствующие изменения где-то в другом месте. Такой подход трактует части при­ ложения как службы, предлагающие функциональные средства, которые задейству­ ются в других частях приложения. Класс, предоставляющий службу, впоследствии может быть модифицирован или заменен, не требуя внесения изменений в классы. в которых он применяется. Подход более подробно объясняется в главе приложения SportsStore 18, а в случае понадобится создать службу хранилища, которая позволит контроллерам получать реализующие интерфейс IProductReposi tory объекты, не зная, какой класс используется. В итоге появится возможность начать разработку приложения с применением простого класса FakeProductRepository, созданного в предыдущем разделе, и позже заменить его реальным хранилищем, не внося измене­ ния во все классы, которым необходим доступ в хранилище. Службы регистрируются в методе ConfigureServices () класса Startup; в листинге 8.7 определена новая служба для хранилища. Листинг 8.7. Соэдание службы хранилища в файле Startup. cs using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Dependencylnjection; using SportsStore.Models; namespace SportsStore { из папки SportsStore Глава 8. SportsStore: реальное приложение puЫic 215 class Startup { puЬlic void ConfigureServices(IServiceCollection services) { services.AddTransient<IProductRepository, FakeProductRepository>(); services.AddMvc(); void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { puЫic арр, IHostingEnvironment env) { )) ; Добавленный в метод ре ASP.NET Core ConfigureServices () оператор сообщает инфраструкту­ о том, что когда компоненту вроде контроллера необходима реали­ IProductReposi tory, она должна получить экземпляр класса FakeProductReposi tory. Метод AddTransient () указывает, что каждый раз, когда требуется реализация интерфейса IProductRepository, должен создаваться новый объект FakeProductReposi tory. Не беспокойтесь, если смысл кода пока не особен­ зация интерфейса но понятен; вскоре вы увидите, как он вписывается в приложение, а детали того, что происходит, будут представлены в главе 18. Отображение списка товаров Оставшийся материал главы можно было бы посвятить построению модели пред­ метной области и хранилища, вообще не касаясь других частей приложения. Однако такой подход выглядит довольно скучным, поэтому мы пойдем другим путем и при­ ступим к интенсивному использованию MVC, вернувшись к добавлению функцио­ нальности модели и хранилища. когда она понадобится. В текущем разделе нам предстоит создать контроллер и метод действия, который может отображать сведения о товарах в хранилище. Пока ими будут только данные из фиктивного хранилища. но позже мы решим эту задачу. Мы также подготовим на­ чальную конфигурацию маршрутизации, чтобы инфраструктура MVC знала, каким образом сопоставлять запросы в приложении с контроллером. который будет создан. Использование инструмента формирования шаблонов Везде в книге контроллеры и представления мыши на папке в окне Solutioп Explorer, MVC MVC в Visual Studio создаются за счет щелчка правой кнопкой выбора в контекстном меню пункта AddqNew ltem (ДобавитьqНовый элемент) и указания шаблона элемента в открывшемся диалоговом окне Add New ltem (Добавление нового элемента). Существует альтернативный прием, называ­ емый формированием шаблонов контекстном меню Add (scaffolding), при котором среда Visual Studio предлагает в (Добавить) пункты, специально предназначенные для создания кон­ троллеров и представлений. После выбора таких пунктов меню предлагается выбрать сце­ нарий для создаваемого компонента, такой как контроллер с действиями, допускающими чтение/запись, или представление, которое содержит форму, применяемую для создания специфического объекта модели. Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 216 Формирование шаблонов в книге не используется. Код и разметка, генерируемые инстру­ ментом формирования шаблонов, являются настолько общими, что оказываются едва ли не бесполезными, в то время как набор подцерживаемых сценариев ограничен и не решает распространенные задачи разработки. Цель настоящей книги знания, каким образом создавать приложения MVC, не только донести до вас - но также объяснить, как все работает "за кулисами", и сделать это гораздо труднее, когда ответственность за создание компо­ нентов возлагается на инструмент формирования шаблонов. Тем не менее, мы имеем еще одну ситуацию, когда ваш стиль разработки может отличаться от моего, и в своих проектах вы вполне можете предпочесть работу с инструментом форми­ рования шаблонов. Ваше решение полностью приемлемо, хотя я рекомендую уделить время на выяснение того, что именно инструмент формирования шаблонов делает, чтобы знать, куда затем смотреть, если будут получены не те результаты, которые ожидались. Добавление контроллера Чтобы создать первый контроллер в приложении. добавим в папку файл класса по имени тинге ProductController. cs Controllers с определением. показанным в лис­ 8.8. Листинг 8.8. Содержимое файла ProductController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; namespace SportsStore.Controllers class ProductController : Controller private IProductRepository repository; puЫic ProductController(IProductRepository repo) { repository = repo; puЫic Когда са инфраструктуре ProductController MVC необходимо для обработки создать новый НТТР-запроса, экземпляр клас­ проинспектирует MVC конструктор и выяснит, что он требует объекта. который реализует интерфейс IProductReposi tory. Чтобы определить, какой класс реализации должен использо­ ваться, инфраструктура MVC обращается к конфигурации в классе сообщает о том, что нужно применять класс FakeReposi tory его новый экземпляр. Инфраструктура МVС создает новый объект использует его для вызова конструктора Startup, которая и каждый раз создавать ProductController FakeReposi tory и с целью создания объ­ екта контроллера, который будет обрабатывать НТТР-запрос. Такой подход известен под названием внедрение зависимостей и позволяет объек­ ту ProductController получать доступ к хранилищу приложения через интерфейс IProductReposi tory без необходимости в знании того, какой класс реализации был сконфигурирован. Позже мы заменим фиктивное хранилище реальным. а благодаря внедрению зависимостей контроллер продолжит работать безо всяких изменений. Глава 8. SportsStore: реальное приложение 217 На заметку! Некоторым разработчикам не нравится внедрение зависимостей, поскольку они считают, что оно усложняет приложения. Я не придерживаюсь такой точки зрения, но если вы не знакомы с внедрением зависимостей, тогда рекомендую вам прочитать главу 18, прежде чем принимать решение. Далее добавим метод действия по имени List (),который будет визуализировать представление, отображающее полный список товаров из хранилища (листинг Листинг 8.9. Добавление метода действия в файле из папки 8.9). ProductController. cs Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; namespace SportsStore.Controllers class ProductController : Controller private IProductRepository repository; puЬlic ProductController(IProductRepository repo) { repository = repo; puЫic puЫic ViewResul t List () => View (reposi tory. Products) ; } Вызов метода View () щает инфраструктуре подобного рода (без указания имени представления) сооб­ MVC о том, что нужно визуализировать стандартное представ­ ление для метода действия. Передача методу View () коллекции объектов Product из хранилища снабжает инфраструктуру данными, которыми необходимо заполнить объект Model в строго типизированном представлении. Добавление и конфигурирование представления Нам требуется создать представление для отображения содержимого пользовате­ лю. но предстоит проделать ряд подготовительных шагов, чтобы упростить написа­ ние представления. Сначала понадобится создать разделяемую компоновку, в кото­ рой будет определено общее содержимое, включаемое во все отправляемые клиентам НТМL-ответы. Разделяемые компоновки - удобный способ обеспечить согласован­ ность представлений и наличие в них важных файлов CSS; их работа объяснялась в главе Создадим папку Layout Page Views/Shared JavaScript и таблиц стилей 5. и добавим в нее новый файл с шаблоном (Страница компоновки представлений MVC) MVC View и именем_ Layout. cshtrnl, Visual Studio элементу Layout. cshtrnl. В стан­ которое является стандартным именем, назначаемым средой такого типа. В листинге 8.10 приведено содержимое файла дартное содержимое внесено одно изменение, связанное с установкой внутренностей элемента Листинг ti tle 8.1 О. в Содержимое файла_Layout. < ! DOCTYPE html> <html> SportsStore. cshtml из папки Views/Shared 218 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 <head> <meta name="viewport" content="width=device-width" /> <title>Sportsstore</title> </head> <body> <div> @RenderBody () </div> </body> </html> Затем необходимо сконфигурировать приложение. чтобы файл применялся по умолчанию, для чего в папку View Start Page Layout. cshtml добавляется файл с шаблоном MVC (Файл запуска представления Стандартное содержимое. добавляемое новку по имени Листинг Views MVC) и именем ViewStart. cshtml. Visual Studio (листинг 8.11 ). выбирает компо­ которая соответствует файлу из листинга Layout. cshtml, 8.10. 8.11. Содержимое файла_ViewStart. cshtml из папки Views @{ Layout " Layout"; Теперь нужно добавить представление, которое будет отображаться. когда для обра­ ботки запроса используется метод действия List и добавим в нее файл представления по имени разметку, показанную в листинге Razor 8. 12. ().Создадим папку List. cshtml. Views/Product Поместим в него Листинг 8.12. Содержимое файла List. cshtml из папки Views/Product @model IEnumeraЫe<Product> @foreach (var <div> р in Model) <hЗ>@p.Name</hЗ> @p.Description <h4>@p.Price.ToString("c")</h4> </div> Выражение @model в начале файла указывает, что представление будет получать от метода действия последовательность объектов С помощью выражения @foreach Product в качестве данных модели. осуществляется проход по этой последовательнос­ ти с генерацией простого набора НТМL-элементов для каждого полученного объекта Product. Представление не знает. откуда поступили объекты Product, как они были полу­ чены или охватывают ли они все товары, известные приложению. Взамен представ­ ление имеет дело только со способом отображения деталей каждого объекта Product с применением НТМL-элементов, что согласуется с принципом разделения обязаннос­ тей, который был описан в главе 3. Глава 8. SportsStore: реальное приложение Совет. Значение свойства ToS tr ing ( 11 с 11 ) , Price 219 преобразуется в строку с использованием метода который визуализирует числовые значения в виде денежных зна­ чений в соответствии с настройками культуры, действующими на сервере. Например, если в качестве настройки культуры сервера установлено ToString ( 11 с 11 ) возвратит культура en-GB, тогда тот же значение $1, 002. 30, самый вызов возвратит en-US, то вызов ( 1002. 3) . но если для сервера установлена f.l, 002. 30. Установка стандартного маршрута Инфраструктуре MVC понадобится сообщить, что она должна отправлять запросы, постуnающие для корневого URL приложения (ht tp: //мой-сайт/), мето.цу действия List () классе класса ProductController. Задача решается редактированием оператора в Startup, который настраивает классы MVC, обрабатывающие НТГР-запросы 8.13). (листинг 8.1 З. Изменение SportsStore Листинг using using using using using using using using using стандартного маршрута в файле Startup. cs из папки System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; SportsStore.Models; namespace SportsStore { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddTransient<IProductRepository, FakeProductRepository>(); services.AddMvc(); puЫic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); puЬlic app.UseМvc(routes арр, IHostingEnvirorunent env) { => { routes.МapRoute( name: "default", template: "{controller=Product}/{action=List}/{id?}"); }) ; 1 Метод Configure () класса Startup применяется для настройки конвейера за­ просов, состоящего из классов (известных как промежуточное программное обеспе­ чение), которые бу.цут инспектировать НТГР-запросы и генерировать ответы. 220 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Метод UseMvc () настраивает промежуточное программное обеспечение MVC. при­ чем одним из параметров конфигурации является схема. которая будет использовать­ ся для сопоставления URL с контроллерами и методами действий. Система маршру­ тизации подробно рассматривается в главах изменения, выделенные в листинге 8 . 13, мости отправки запросов методу действия в URL а пока просто следует знать, что List () контроллера о необходи­ MVC Pr oduct , если только запроса не указано иное. Совет. Обратите внимание, что в листинге не 15 и 16. сообщают инфраструктуре Produ ctContro ll er , являющееся вании вом MVC, 8.13 имя контроллера установлено в Product, а именем класса . Это часть соглашения об имено­ в рамках которого имена классов контроллеров обычно заканчиваются сло­ Contr olle r, но при ссылке на класс данная часть имени опускается . Соглашение об именовании и его влияние объясняются в главе 31. Запуск приложения Все основные компоненты в наличии . Мы имеем контроллер с методом действия. который MVC будет применять, когда запрашивается стандартный жения. Инфраструктура MVC создаст экземпляр класса URL FakeReposi tory. для прило­ после чего будет использовать его для создания нового объекта контроллера, обрабатывающего запрос. Фиктивное хранилище снабдит контроллер простыми тестовыми данными , которые его метод действия передаст представлению браузера будет включать детали каждого объекта ответа инфраструктура MVC Razor, так что НТМL-ответ для Pr od uct. При генерации НТМL­ объединит данные из представления, выбранные ме­ тодом действия, с содержимым разделяемой компоновки. порождая завершенный НТМL-документ, который браузер в состоянии разобрать и отобразить. Запустив при­ ложение , можно увидеть результат, показанный на рис. 8.7. Итак, мы исследовали типовой шаблон разработки для инфраструктуры Саге MVC. ASP.NET Начальные затраты времени на необходимую настройку являются обяза­ тельными, но затем основные средства приложения будут собираться очень быстро. С) SportsSlor• CD С х localhost:53406 Football S25.00 Sш·f boa1·d Sl 79.00 Rlшning shoes S95.00 Рис. 8. 7. Просмотр основной функциональности приложения Глава 8. SportsStore: реальное nриложение 221 Подготовка базы данных Мы способны отображать простое представление, содержащее сведения о товарах, но применяем тестовые данные, которые находятся в фиктивном хранилище. Прежде чем можно будет реализовать хранилище с реальными данными, необходимо настро­ ить базу данных и заполнить ее данными. SQL Server, а доступ к ней будет осущест­ Framework Core (EF Core) - инфраструктуры объект­ отображения (object-relational mapping - ORM) для Microsoft .NET. Для базы данных будет использоваться вляться с применением Eпtity но-реляционного Инфраструктура ОRМ представляет таблицы, столбцы и строки реляционной базы данных посредством обычных объектов С#. На заметку! Это область, где можно выбирать из широкого диапазона инструментальных средств и технологий. Доступны не только различные реляционные базы данных, но мож­ но также работать с хранилищами объектов, хранилищами документов и рядом экзотичес­ ких альтернатив. Кроме того, существуют другие инфраструктуры ORM для .NЕТ, каждая из которых использует слегка отличающийся подход; такие вариации позволяют выбрать то, что лучше всего подходит для конкретных проектов. Инфраструктура Entity Framework Core применяется по нескольким причинам: LINQ (а мне нравится ASP.NET Core MVC. Ранним выпускам ее просто запустить в работу, она прекрасно интегрируется с использовать EF Core LINQ) и она хорошо работает с были присущи мелкие недостатки, но текущие версии элегантны и богаты возможностями. Продукт SQL Server располагает удобным средством Loca/DB, которое представля­ ет собой не требуюIЦУю администрирования реализацию основной функциональнос­ ти SQL Server, специально спроектированную для разработчиков. Благодаря LocalDB можно пропускать процесс настройки базы данных на время построения проекта, а развертывание проводить в полном экземпляре ложений MVC SQL Server позже. Большинство при­ развертываются в размещаемых средах, которые обслуживаются про­ фессиональными администраторами, и наличие средства LocalDB означает, что кон­ фигурирование баз данных остается в руках администраторов баз данных, позволяя разработчикам приложений заниматься написанием кода. Visual Studio вы не выбрали LocalDB, то придется сделать это LocalDB может быть выбрано через раздел lпdividual Compoпeпts (Индивидуальные компоненты) в программе установки Visual Studio. Если вы следовали инструкциям, приведенным в главе 2, тогда средство LocalDB должно быть установлено Совет. Если при установке сейчас. Средство и готово к применению. Установка пакета инструментов командной строки Entity Framework Core Основная функциональность чанию, когда кет NuGet, Visual Studio Entity Framework Core добавляется в проект по умол­ создает проект. Нам необходим один дополнительный па­ предоставляющий инструменты командной строки, которые используются для создания классов, готовящих базу данных к хранению данных приложения. Такие классы известны как миграции. 222 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Чтобы добавить пакет в проект. щелкнем правой кнопкой мыши на элементе проекта SportsStore в окне Solutioп Explorer. выберем в контекстном меню пункт Edit SportsStore.csproj (Редактировать SportsStore. csproj изменения, показанные в листинге SportsStore. csproj) 8.14. и внесем в файл Важно применять версию, указанную в листинге. Кроме того, пакет добавляется с использованием эле­ мента DotNetCli ToolReference, а не PackageReference, который предназначен для добавления существующего пакета. На заметку! Пакет инструментов командной строки Eпtity ливаться за счет редактирования файла Framework Core SportsStore. csproj. могут быть добавлены с применением диспетчера пакетов NuGet должен устанав­ Пакеты такого типа не либо инструмента ко­ мандной строки dotпet. Листинг 8.14. Добавление пакета в файле из папки SportsStore. csproj SportsStore <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <DotNetCliToolReference Version="2.0.0" /> Include="Мicrosoft.EntityFrameworkCore.Tools.DotNet" </ItemGroup> </Project> После сохранения файла среда ментов командной строки Visual Studio загрузит и установит пакет инстру­ Entlty Framework Core, а также добавит их в проект. Создание классов базы данных Класс контекста базы данных - это шлюз между приложением и EF Core, кото­ рый обеспечивает доступ к данным приложения с использованием объектов моделей. Чтобы создать класс контекста базы данных для приложения в папку Models файл класса по имени приведенным в листинге Листинг 8.15. добавим с определением, 8.15. Содержимое файла ApplicationDbContext.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Dependencyinjection; namespace SportsStore.Models { puЬlic SportsStore, ApplicationDbContext. cs class ApplicationDbContext DbContext { из папки Мodels Глава 8. SportsStore: реальное приложение 223 ApplicationDbContext(DbContextOptions<ApplicationDbCon text> options) : base ( options) { ) puЫic DbSet<Product> Products { get; set; ) puЫic Базовый класс DbContext предоставляет доступ к лежащей в основе функцио­ а свойство Products обеспечивает доступ к объек­ Product в базе данных. К.ласе ApplicationDbContext является производным DbContext и добавляет свойства, которые будут применяться для чтения и записи нальности Entity Framework Core, там от данных приложения. В текущий момент имеется только одно свойство, которое пре­ доставит доступ к объектам. Создание класса хранилища Хотя в настоящее время может показаться иначе. но большая часть работы, требу­ емой для настройки базы данных, завершена. Следующий шаr заключается в созда­ нии класса, который реализует интерфейс с использованием инфраструктуры файл класса по имени IProductRepository Entity Framework Core. EFProductReposi tory. cs представленным в листинге и получает данные Добавим в папку Models с определением класса хранилища, 8.16. Листинг В. 1б. Содержимое файла EFProductReposi tory. cs из папки Models using Systern.Collections.Generic; using Systern.Linq; narnespace SportsStore.Models { class EFProductRepository : IProductRepository { private ApplicationDbContext context; puЬlic EFProductRepository(ApplicationDbContext ctx) { context = ctx; puЬlic puЫic IQueryaЬle<Product> Products => context.Products; По мере добавления средств к приложению в класс будет добавляться соответству­ ющая функциональность, а пока реализация хранилища просто отображает свойство IProductReposi tory, на свойство Products, ApplicationDbContext. Свойство Products в клас­ се контекста возвращает объект DbSet<Product>, который реализует интерфейс IQueryaЫe<T> и облегчает реализацию интерфейса IProductRepository, когда Products, определенное в интерфейсе которое определено в классе применяется Entity Framework Core. В результате гарантируется, что запросы к базе данных будут извлекать только те объекты, которые требуются, как объяснялось ра­ нее в главе. Определение строки подключения Строка подключения указывает местоположение и имя базы данных, а также предоставляет конфигурационные настройки, с которыми приложение должно под- 224 Часть 1. Введение в инфраструктуру ASP.NEТ Саге MVC 2 ключаться к серверу базы данных. Строки подключения хранятся в файле названием appsettings. j son, нием шаблона элемента который создан в проекте ASP.NET Coпfiguratioп File SportsStore JSON под с использова­ (Конфигурационный файл ASP.NEТ) из раздела Geпeral (Общие) диалогового окна При создании файла Add New ltem. appsettings. j son среда Visual Studio добавляет в него за­ полнитель для строки подключения, который необходимо заменить содержимым лис­ тинга 8.17. Совет. Строка подключения должна быть выражена как единственная неразрывная строка, что выглядит нормально в редакторе Visual Studio, но не умещается на печатной стра­ нице, чем и объясняется неуклюжее форматирование в листинге 8.17. При определе­ нии строки подключения в собственном проекте удостоверьтесь, что значение элемента ConnectionString Листинг 8.17. находится в одной строке. Редактирование строки подключения в файле из папки appsettings. json SportsStore "Data": { "SportStoreProducts": "ConnectionString": "Server= (localdЫ \ \MSSQLLocalDB;DataЬase=SportsStore; Trusted Connection=True;МultipleActiveResultSets=true" } - Внутри раздела Da ta конфигурационного файла имя строки подключения устанав­ SportStoreProducts. Значение элемента ConnectionString указывает, базы данных по имени SportsStore должно применяться средство Loca\DB. ливается в что для Конфигурирование приложения Далее необходимо прочитать строку подключения и сконфигурировать приложе­ ние для ее использования при подключении к базе данных. В листинге 8.18 показаны изменения, которые потребуется внести в класс ных конфигурации, содержащихся в файле конфигурирования Eпtity обеспечивается классом Листинг 8. 18. (Работа, связанная с чтением файла который будет описан в главе Конфигурирование приложения в файле из папки using using using using using using using using using Framework Core. Program, Startup, чтобы получать детали дан­ appsettings. j son, и применять их для s tartup. cs SportsStore System; System.Collections.Gener ic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Bui lder; Microsoft.AspNetCore.Hos ting; Microsoft.AspNetCore.Http ; Microsoft.Extensions.Dep endencyinjection; SportsStore.Models; usingМicrosoft.Extensions.Confiquration; 14.) JSON, Глава 8. SportsStore: реальное приложение 225 usin9Мicrosoft.EntityFrameworkCore; namespace SportsStore { class Startup { puЫic Startup(IConfiquration confiquration) => Confiquration = confiquration; puЫic IConfiquration Confiquration ( 9et; } puЫic void ConfigureServices(IServiceCollection services) => options.UseSqlServer( Confiquration["Data:SportStoreProducts:ConnectionStrin9 "])); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddMvc(); puЫic services.AddDЬContext<ApplicationDЬContext>(options puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Product}/{action=List}/{id?}"); }) ; Startup конструктор получает данные конфигурации из appset tings. j son, которые представлены посредством объекта, реализу­ ющего интерфейс IConfiguration. Конструктор присваивает объект реализации IConfiguration свойству по имени Configuration, так что он может использо­ ваться в остальном коде класса Startup. Добавленный в класс файла 14 будет показано, как читать и обращаться к данным конфигурации. Для SportsStore мы добавили в метод ConfigureServices () последователь­ вызовов методов, которая настраивает Entity Framework Core: В главе приложения ность services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Confiquration["Data:SportStoreProducts:ConnectionStrin9 "])); Расширяющий метод AddDbCon text () настраивает службы, предоставляемые создан в Entity Framework Core, для контекста базы данных, который был листинге 8.15. Как объясняется в главе 14, многие методы, применяемые в классе Startup, инфраструктурой позволяют конфигурировать службы и средства промежуточного программного обеспечения с использованием аргументов да AddDbContext () options. Аргументом мето­ является лямбда-выражение, которое получает объект options, конфигурирующий базу данных для класса контекста. В этом случае база данных конфигурируется с помощью метода ния, которая получена из свойства UseSqlServer () Configuration. и указания строки подключе­ 226 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Еще одним изменением, внесенным в класс была замена фиктивного Startup, хранилища реальным: services. AddTransient<IProductReposi tory, EFProductRepository> () ; Компоненты в приложении, работающие с интерфейсом которым в настоящий момент относится только контроллер будvт получать объект EFProductReposi tory. IProductRepository. к Product, при создании предоставляющий им доступ к инфор­ мации в базе данных. Подробные объяснения приводятся в главе 18, а пока просто знайте, что результатом будет гладкая замена фиктивных данных реальными из базы данных без необходимости в изменении класса ProductController. Отключение проверки области видимости Применение Entity Framework Core требует внесения изменения в конфигура­ цию средства внедрения зависимостей, которое рассматривается в главе Класс несет ответственность за запуск и конфигурирование перед Program передачей управления классу Startup, и в листинге 8.19 18. ASP.NET Core показано необходимое из­ менение. Без такого изменения попытка создать схему базы данных в следvющем раз­ деле приведет к генерации исключения. Листинг 8. 19. Подготовка для работы с Eпtity из папки using using using using using using using using using Framework Core в файле Program. cs SportsStore System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; namespace SportsStore ( puЫic class Program ( puЬlic static void Main(string[] args) ( BuildWebHost(args) .Run(); static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .UseDefaultServiceProvider(options => options.ValidateScopes = false) . Build (); puЫic Конфигурирование ASP.NET Core подробно объясняется в главе 14, а прос­ то отметим, что это единственное изменение, которое требуется для приложения SportsStore. Глава 8. SportsStore: реальное приложение 227 Создание миграции базы данных Framework Core способна Инфраструктура Eпtity генерировать схему для базы дан­ ных, используя классы моделей, с помощью средства, которое называется миграция­ ми. При подготовке миrрации инфраструктура команды EF Core создает класс С#, содержащий которые нужны для подготовки базы данных. Если необходимо моди­ SQL, фицировать классы моделей, тогда можно создать новую миграцию, которая содер­ жит команды SQL, требуемые для отражения изменений. В результате не приходится беспокоиться о написании вручную и тестировании команд SQL и можно сосредото­ читься на классах модели С# в приложении. Команды Eпtity Framework Core выполняются из командной строки. Оrкроем окно ко­ PowerShell, перейдем в папку проекта SportsStore (ту, что со­ мандной строки или окно держит файлы Startup. cs и appsettings. j son) и запустим следУЮщую команд.У. что­ бы создать класс миграции. который подготовит базу данных к первому использованию: dotnet ef migrations add Initial Когда команда завершит свое выполнение, в окне Solution Explorer среды Visual Studio появится папка Migrations. Именно в ней инфраструктура Eпtity Framework Core хранит классы миграции. Одно из имен файлов будет выглядеть как отметка времени, за которой следует_ Ini tial. cs, и в этом файле определен класс, приме­ няемый для создания начальной схемы базы данных. Просмотрев содержимое тако­ го файла, можно выяснить, каким образом класс модели Product. использовался для создания схемы. Что насчет команд Add-Мigration и Update-DataЬase? Если вы имеете опыт разработки с помощью Eпtity нению команды Add-Migration для Framework, то могли привыкнуть к приме­ создания миграции базы данных и команды Update- Database для ее применения к базе данных. С появлением платформы .NET Core в инфраструктуру Entity Framework Core были добавле­ dotnet, который использу­ Microsoft. Enti tyFrameworkCore. Tools. DotNet, добавленный к проекту ны команды, интегрированные в инструмент командной строки ет пакет в листинге 8.14. Такие команды применяются в настоящей главе, поскольку они согласуются с другими командами PowerShell .NET и могут использоваться в любом окне командной строки или в отличие от команд только в специальном окне Add-Migration и Update-Database, которые работают Visual Studio. Создание начальных данных Чтобы заполнить базу данных и предоставить какие-то тестовые данные, добавим в папку Models листинге Листинг using using using using файл класса по имени SeedData. cs с определением, приведенным в 8.20. 8.20. Содержимое файла SeedData. cs из папки System.Linq; Microsoft.AspNetCore.Builder; Microsoft.Extensions.Dependencylnjection; Microsoft.EntityFrameworkCore; namespace SportsStore.Models { Models 228 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 puЫic static class SeedData ( static void EnsurePopulated(IApplicationBuilder арр) ApplicationDbContext context = app.ApplicationServices .GetRequiredService<ApplicationDbContext>(); context.Database.Migrate(); 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 = "Protective and fashionaЫe", Category = "Watersports", Price = 48.95m ), new Product ( Name = "Soccer Ball", Description = "FIFA-approved size and weight", Category = "Soccer", Price = 19. 50m } , new Product { Name = "Corner Flags", Description = "Give your playing field а professional touch", Category = "Soccer", Price = 34.95m }, new Product { Name = "Stadium", Description = "Flat-packed 35, 000-seat stadium", Category = "Soccer", Price = 79500 }, new Product { Name = "Thinking Сар", Description = "Improve brain efficiency Ьу 75i", Category = "Chess", Price = 16 } , new Product { Name = "Unsteady Chair", Description = "Secretly give your opponent а disadvantage", Category = "Chess", Price = 29.95m }, new Product ( Name = "Human Chess Board", Description = "А fun game for the family", Category = "Chess", Price = 75 } , new Product ( Name = "Bling-Bling King", Description = "Gold-plated, diamond-studded King", Category = "Chess", Price = 1200 puЫic } ) ; context.SaveChanges(); Статический метод EnsurePopula ted () получает аргумент типа IAppl i са t i on Builder. представляющий собой интерфейс, который применяется в методе Configure () класса Startup для регистрации компонентов промежуточного про- Глава 8. SportsStore: реальное приложение 229 граммного обеспечения, обрабатывающих НТТР-запросы, и именно здесь будет обес­ печено наличие в базе данных содержимого. EnsurePopulated () получает объект ApplicationDbContext через ин­ IApplicationBuilder и вызывает метод Database .Migrate (),чтобы га­ Метод терфейс рантировать применение миграции, что означает создание и подготовку базы данных к хранению объектов Product. Далее проверяется количество объектов Product в базе данных. Если объекты в базе данных отсутствуют, то база данных заполняется с использованием коллекции объектов затем записывается с помощью метода Product посредством SaveChanges (). метода AddRange () и Финальное изменение заключается в заполнении базы данных начальной инфор­ мацией при запуске приложения, для чего в класс EnsurePopulated (),как Листинг Startup добавляется вызов метода 8.21. 8.21. Заполнение базы данных начальной информацией в файле using using using using using using using using using using using показано в листинге s tartup. св из папки Sportss tore System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; SportsStore.Models; Microsoft.Extensions.Configuration; Microsoft.EntityFrameworkCore; namespace SportsStore { class Startup { Startup(IConfiguration configuration) => Configuration = configuration; puЫic IConfiguration Configuration { get; ) puЬlic void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(optio ns => options.UseSqlServer( Configuration["Data:SportStoreProducts:Connectio nString"J) ); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddMvc(); puЫic puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => ( routes.MapRoute( name: "default", template: "{controller=Product)/{action=List)/{id?}"); puЫic }) ; Seed.Data.EnsurePopulated(app); 1 230 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 В результате запуска приложения база данных создается. заполняется и применя­ ется для обеспечения приложения своими данными . (Запаситесь терпением : создание базы данных может занять некоторое время). URL Когда браузер запрашивает стандартный ция приложения сообщает MVC для приложения, конфигура­ о необходимости создания контроллера для обработки запроса. Создание контроллера Product Product означает вызов конструк­ тора класса ProductC o ntr o ller. которому требуется объект. реализующий ин­ IProductR epo si t o ry. и новая конфигурация указывает MVC о том. что для этого должен быть создан и применен объект EFProductReposi tory. Объект EFProductReposi tory обращается к функциональности Entity Framework Core. которая загружает данные из SQL Server и преобразует их в объекты Pro d uct. Вся упомянутая работа скрыта от класса ProductControl l er. который просто получает объект. реализующий интерфейс IPro ductRe p ository. и пользуется данными, ко­ терфейс торые он предоставляет. В итоге окно браузера отображает тестовую информацию из базы данных (рис. 8.8). D SportsSto~ С х Ф localhost 534{)5 Kayak А boat for 011е pe!'SO!l S275.00 Lift-jacket Protective авd fasllio11aЫe S48.95 Soccei· Ball FIFA-appro\'ed size a11d \veigllt Рис. 8.8. Использование хранилища в виде базы данных Такой подход с применением ных SQL Server Entity Framework Core для представления базы дан­ в виде последовательности объектов моделей отличается простотой ASP.NET Core EF Core, и большое и легкостью, позволяя сосредоточить внимание на инфраструктуре MVC. Я опустил множество деталей, касающихся оперирования количество доступных конфигурационных параметров. Мне нравится инфраструкту­ ра Entity Framework Core, и я рекомендую уделить время на ее изучение . Хорошей отправной точкой послужит веб-сайт Microsoft для docs.microsoft.com/ru-ru/ef / #pivot=efcore) Framework Саге, которая выйдет в издательстве Entity Framework Саге (h t tps: / / Entity или моя будущая книга по Apress. Глава 8. SportsStore: реальное приложение 231 Добавление поддержки разбиения на страницы 8.8 На рис. видно, что представление List. cshtml отображает сведения о товарах в базе данных на одной странице. В этом разделе мы добавим поддержку разбиения на страницы, чтобы представление отображало на странице меньшее число товаров, а пользователь мог переходить со страницы на страницу для просмотра всего ката­ лога. Для решения задачи добавим в метод действия параметр (листинг Листинг 8.22. List () контроллера Product 8.22). Добавление поддержки разбиения на страницы в файле ProductController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; namespace SportsStore.Controllers class ProductController : Controller private IProductRepository repository; puЫic puЫic int PageSize = 4; ProductController(IProductRepository repo) ( repository = repo; puЫic puЫic ViewResul t List (int productPage = 1) => View(repository.Products .OrderBy(p => p.ProductID) . Skip ( (productPage - 1) .Take(PageSize)); Поле PageSize * PageSize) указывает, что на одной странице должны отображаться сведения о четырех товарах. В метод List () добавлен необязательный параметр, который означает, что в случае вызова метода без параметра (List ())вызов трактуется так, словно ему было передано значение, указанное в определении параметра (List ( 1) ). В результате метод действия отображает первую страницу сведений о товарах, когда инфраструктура MVC получаем объекты вызывает его без аргумента. Внутри тела метода действия мы Product, упорядочиваем их по первичному ключу, пропускаем то­ вары, которые располагаются до начала текущей страницы, и выбираем количество товаров, указанное в поле PageSize. Модульное тестирование: разбиение на страницы Модульное тестирование средства разбиения на страницы можно провести, создав имитированное хранилище, внедрив его в конструктор класса L i s t ( ) , чтобы Product можно сравнить ProductController и вызвав метод запросить конкретную страницу. Затем полученные объекты с теми, которые ожидались от тестовых данных в ими­ тированной реализации. Написание модульных тестов обсуждалось в главе 7. Ниже пока­ зан созданный для этой цели модульный тест, который находится в файле класса по имени ProductControllerTests. cs, добавленном в проект SportsStore. Tests: 232 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 using using using using using using Systern.Collections.Generic; Systern.Linq; Moq; SportsStore.Controllers; SportsStore.Models; Xunit; narnespace SportsStore.Tests puЫic class ProductControllerTests [Fact] puЬlic 11 void Can_Paginate() { Организация Mock<IProductRepository> rnock = new Mock<IProductRepository>(); rnock.Setup(rn => rn.Products) .Returns( (new Product[] new Product {ProductID 1, Narne "Pl"}, new Product {ProductID 2, Narne "Р2"}, new Product {ProductID 3, Narne "Р3"}, new Product {ProductID 4, Narne "Р4"}, new Product {ProductID S, Narne "PS"} )) .AsQueryaЬle<Product>()); ProductController controller controller.PageSize = 3; 11 new ProductController(rnock.Object); Действие result controller.List(2) .ViewData.Model as IEnurneraЫe<Product> 11 IEnurneraЫe<Product>; Утверждение Product[) prodArray = result.ToArray(); Assert.True(prodArray.Length == 2); Assert.Equal ("Р4", prodArray[O] .Narne); Assert.Equal ("PS", prodArray[l] .Narne); Получение данных, возвращаемых из метода действия, выглядит несколько неуклюже. Результатом является объект ViewResult, и значение его свойства должно быть приведено к ожидаемому типу данных. В главе ViewData.Model 17 рассматриваются разнооб­ разные результирующие типы, которые могут возвращать методы действий, а также спосо­ бы работы с ними. Отображение ссылок на страницы Запустив приложение, вы увидите, что теперь на странице отображаются четыре позиции каталога. Если нужно просмотреть другую страницу, тогда в конец URL мож­ но добавить параметр строки запроса, например: http://localhost:S000/?productPage=2 Вы должны указать в URL номер порта, который был назначен вашему проекту. Используя такие строки запросов, можно перемещаться по каталогу товаров. У пользователей нет какого-либо способа выяснить о существовании таких пара­ метров строки запроса, но даже если бы он был, то вряд ли бы они захотели иметь 233 Глава 8. SportsStore: реальное приложение дело с навигацией подобного вида. Взамен мы должны визуализировать в нижней части каждого списка товаров ссылки на страницы, чтобы пользователи могли пе­ реходить между страницами. Мы реализуем вспомогательную функцию дескриптора, которая будет генерировать НТМL-разметку для требуемых ссьmок. Добавление модели представления Чтобы обеспечить поддержку вспомогательной функции дескриптора, мы собира­ емся передавать представлению информацию о количестве доступных страниц, теку­ щей странице и общем числе товаров в хранилище. Проще всего это сделать, создав класс модели представления, который специально применяется для передачи дан­ ных между контроллером и представлением. Создадим в проекте ку Models/ViewModels листинге 8.23. Листинг 8.23. SportsStore пап­ и добавим в нее файл класса с содержимым, приведенным в Содержимое файла Paqinqinfo. cs из папки Мodels/ViewModels using System; namespace SportsStore.Models.ViewModels class int puЫic int puЫic int puЫic puЬlic Paginginfo { Totalitems { get; set; } ItemsPerPage { get; set; CurrentPage { get; set; } int TotalPages => (int)Math.Ceiling( (decimal)Totalitems / ItemsPerPage); puЫic Добавление класса вспомогательной функции дескриптора Теперь, имея модель представления, можно создать вспомогательную функцию SportsStore папку Infrastructure и добавим PageLinkTagHelper. cs с определением. показанным дескриптора. Создадим в проекте в нее файл класса по имени в листинге 8.24. Вспомогательные функции дескрипторов являются крупной частью инфраструктуры ASP.NEТ Core МVС, и в главах 23-25 объясняется, как они работают и каким образом их создавать. Совет. В папку Infrastructure будут помещаться классы, которые предоставляют прило­ жению связующий код, но не имеют отношения к предметной области приложения. Листинг 8.24. Содержимое файла из папки using using using using using using PaqeLinkTaqHelper. cs Infrastructure Microsoft.AspNetCore.Mvc; Microsoft.AspNetCore.Mvc.Rendering; Microsoft.AspNetCore.Mvc.Routing; Microsoft.AspNetCore.Mvc.ViewFeatures; Microsoft.AspNetCore.Razor.TagHelpers; SportsStore.Models.ViewModels; 234 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 namespace SportsStore.Infrastructure { [HtmlTargetElement("div", Attributes = "page-model")] class PageLinkTagHelper : TagHelper { private IUrlHelperFactory urlHelperFactory; puЫic PageLinkTagHelper(IUrlHelperFactory helperFactory) urlHelperFactory = helperFactory; puЬlic [ViewContext] [HtmlAttributeNotBound] puЬlic ViewContext ViewContext { get; set; puЬlic Paginglnfo PageModel { get; set; puЬlic string PageAction { get; set; } override void Process(TagHelperContext context, 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 = urlHelper.Action(PageAction, new { productPage = i )) ; tag.InnerHtml.Append(i.ToString()); result.InnerHtml.AppendHtml(tag); puЫic output.Content.AppendHtml(result.InnerHtml); Класс вспомогательной функции дескриптора элемент di v PageLinkTagHelper заполняет элементами а, которые соответствуют страницам товаров. Сейчас мы не будем вдаваться в какие-либо детали относительно вспомогательных функций де­ скрипторов: достаточно знать. что они предлагают один из наиболее удобных спосо­ бов помещения логики С# в представления. Код для вспомогательной функции дескриптора может выглядеть запутанным, по­ тому что смешивать С# и HTML непросто. Но использование вспомогательных функ­ ций дескрипторов является предпочтительным способом включения блоков кода С# в представление, поскольку вспомогательную функцию дескриптора можно легко под­ вергать модульному тестированию. Большинство компонентов MVC, таких как контроллеры и представления, обнару­ живаются автоматически, но вспомогательные функции дескрипторов должны быть зарегистрированы. В листинге ки Views 8.25 приведено содержимое файла Viewlmports. cshtml с добавленным оператором, который сообщает MVC тельные функции дескрипторов следует искать в пространстве имен Infrastructure. Кроме того, добавлено также выражение из пап­ о том, что вспомога­ @using. SportsStore. чтобы на 1U1ассы моделей представлений можно было ссылаться в представлениях, не указывая про­ странство имен. Глава 8. SportsStore: реальное nриложение Листинг 8.25. 235 Регистрация вспомогательной функции дескриптора в файле_Viewimports. cshtml из папки Views/Home @using SportsStore.Models @using SportsStore.Мodels.ViewМodels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTaqHelper SportsStore.Infrastructure.*, SportsStore Модульное тестирование: создание ссылок на страницы Чтобы протестировать класс вспомогательной функции дескриптора вызывается метод Р r TagHelperOutput, о се ss () с тестовыми данными и PageLinkTagHelper, предоставляется объект который инспектируется на предмет сгенерированной разметки. Тест определен в новом файле PageLinkTagHelperTests. cs НТМL­ внутри проекта 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Ьlic class PageLinkTagHelperTests [Fact] puЫic 11 void Can_Generate Page Links() Организация var urlHelper = new Mock<IUrlHelper>(); urlHelper.SetupSequence(x => x.Action(It.IsAny<UrlActionContext>())) .Returns("Test/Pagel"J .Returns("Test/Page2") .Returns("Test/Page3"); var urlHelperFactory = new Mock<IUrlHelperFactory>(); urlHelperFactory.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())) .Returns(urlHelper.Object); PageLinkTagHelper helper = new PageLinkTagHelper(urlHelperFactory.Object) PageModel = new Paginginfo { CurrentPage = 2, Totalitems = 28, ItemsPerPage = 10 }, PageAction = "Test" 1; TagHelperContext ctx = new TagHelperContext( new TagHelperAttributeList(), new Dictionary<object, object>(), ""); 236 Часть 1. Введение в инфраструктуру ASP NET Саге MVC 2 var content = new Mock<TagHelperContent>(); TagHelperOutput output = new TagHelperOutput ( "div", new TagHelperAttributeList(), (cache, encoder) => Task.FromResult(content.Object)); 11 Действие helper.Process(ctx, output); 11 Утверждение Assert.Equal(@"<a href=""Test/Pagel"">l</a>" + @"<а href=""Test/Page2"">2</a>" +@"<а href=""Test/Page3"">3</a>", output.Content.GetContent() ); Сложность теста связана с построением объектов, которые требуются для создания и применения вспомогательной функции дескриптора. Вспомогательные функции дескрип­ торов используют объекты реализаций IUrlHelperFactory для генерации URL, ко­ торые указывают на разные части приложения. Для создания реализаций интерфейса IUrlHelperFactory и связанного с ним данные, используется инфраструктура IUrlHelper, предоставляющего тестовые Moq. Основная часть теста проверяет вывод вспомогательной функции дескриптора с приме­ нением литерального строкового значения, которое содержит двойные кавычки. Язык С# позволяет работать с такими строками при условии, что строка предварена символом @, а вместо одной двойной кавычки внутри строки используется набор из двух двойных кавычек (" "). Вы должны помнить о том, что разносить литеральную строку по нескольким строкам файла нельзя, если только строка, с которой производится сравнение, не разнесена ана­ логичным образом. Например, литерал, применяемый в тестовом методе, был размещен в нескольких строках из-за недостаточной ширины печатной страницы. Символ новой строки не добавлялся, иначе тест не прошел бы. Добавление данных модели представления Мы пока не готовы использовать вспомогательную функцию дескриптора, т.к. пред­ ставление еще не снабжено экземпляром класса модели представления Это можно было бы сделать с применением объекта ViewBag, Paginginfo. но лучше поместить все данные, подлежащие передаче из контроллера в представление, внутрь единствен­ ного класса модели представления. Добавим в папку SportsStore файл класса по имени приведенным в листинге Листинг 8.26. Models/ViewModels проекта ProductsListViewModel. cs с содержимым. 8.26. Содержимое файла из папки ProductsListViewModel. cs Models/ViewModels using System.Collections.Generic; using SportsStore.Models; namespace SportsStore.Models.ViewModels puЫic class ProductsListViewModel { puЬlic IEnumeraЬle<Product> Products get; set; ) puЫic Paginglnfo Paginglnfo { get; set; ) Глава 8. SportsStore: реальное приложение 237 List () класса ProductController, чтобы ProductsListViewModel для снабжения представления сведе­ Далее можно обновить метод действия он использовал класс ниями о товарах, отображаемых на страницах, и информацией о разбиении на стра­ ницы (листинг Листинг 8.27. 8.27). Обновление метода действия в файле List () ProductController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; usinq SportsStore.Models.ViewМodels; namespace SportsStore.Controllers class ProductController : Controller private IProductRepository repository; puЫic int PageSize = 4; puЫic ProductController(IProductRepository repo) { repository = repo; puЫic puЫic ViewResult List(int productPage 1) => View(new ProductsListViewМodel ( Products = repository.Products .OrderВy(p => p.ProductID) . Skip( (productPaqe - 1) * PaqeSize) .Take(PaqeSize), Paqinqinfo = new Paqinqinfo ( CurrentPaqe = productPaqe, ItemsPerPaqe = PaqeSize, Totalitems = repository.Products.Count() }) ; Внесенные изменения ProductsListViewModel обеспечивают передачу представлению объекта как данных модели. Модульное тестирование: данные разбиения на страницы для модели представления Нам необходимо удостовериться в том, что контроллер отправляет представлению коррек­ тную информацию о разбиении на страницы. Ниже показан модульный тест, добавленный в класс ProductControllerTests внутри тестового проекта, который обеспечивает такую проверку: [Fact] void Can Send Pag1nation View Model() { 11 Организаци; Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] { new Product (ProductID = 1, Name = "Pl"}, puЫic Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 238 new new new new }) 11 Product Product Product Product {ProductID {ProductID {ProductID {ProductID .AsQueryaЬle<Product>() 2, 3, 4, 5, ); Name Name Name Name "Р2"}, "Р3"}, "Р4"}, "Р5"} Организация ProductController controller new ProductController(mock.Object) { PageSize 11 3 }; Действие ProductsListViewModel result = controller.List(2) .ViewData.Model as ProductsListViewModel; 11 Утверждение Paginginfo pageinfo = result.Paginginfo; Assert.Equal(2, pageinfo.CurrentPage); Assert.Equal(3, pageinfo.ItemsPerPage); Assert.Equal(5, pageinfo.Totalitems); Assert.Equal(2, pageinfo.TotalPages); Потребуется также модифицировать ранее созданный модульный тест для проверки раз­ биения на страницы, содержащийся в методе Can _ Pag ina te действия List () . Он полагается на метод ViewResul t, свойством Model которого явля­ Product, но мы поместили эти данные внутрь еще од­ (),возвращающий объект ется последовательность объектов ного типа модели представления. Вот переделанный тест: [Fact] puЫic 11 void Can_Paginate() Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[J new Product {ProductID 1, Name "Pl"}, new Product (ProductID 2, Name "Р2"}, new Product {ProductID 3, Name "Р3"}, new Product {ProductID 4, Name "Р4"}, new Product {ProductID 5, Name "Р5"} }) .AsQueryaЫe<Product>() ); ProductController controller controller.PageSize = 3; 11 Действие ProductsListViewМodel controller.List(2) 11 new ProductController(mock.Object); resul t = .ViewData.Мodel as ProductsListViewМodel; Утверждение Product [ J prodArray = resul t. Products. ToArray () ; Assert.True(prodArray.Length == 2); Assert. Equal ( "Р4", prodArray [О] . Name) ; Assert.Equal ("Р5", prodArray[l] .Name); Учитывая объем дублированного кода в двух тестовых методах, обычно следовало бы со­ здать общий метод начальной установки. Однако, поскольку модульные тесты описаны в индивидуальных врезках вроде текущей, их код представлен по отдельности, чтобы с каж­ дым тестом можно было ознакомиться независимо от других. Глава 8. SportsStore: реальное приложение 239 В настоящий момент представление ожидает последовательность объектов Product, ге 8.28, Листинг поэтому файл List. cshtml необходимо обновить, как показано в листин­ чтобы иметь дело с новым типом модели представления. 8.28. Обновление файла List. cshtml из папки Views/Product @model ProductsListViewModel @foreach (var р in Model.Products) <div> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c" )</h4> </div> Выражение @model было изменено для указания механизму перь мы работаем с другим типом данных. Цикл ником данных стало свойство Products foreach Razor на то, что те­ бьm обновлен, чтобы источ­ объекта модели. Отображение ссылок на страницы Наконец-то все готово для добавления в представление List ссылок на страни­ цы. Мы создали модель представления, которая содержит информацию о разбиении на страницы, обновили контроллер, чтобы эта информация передавалась представ­ лению, и модифицировали выражение @model, приведя его в соответствие с новым типом модели представления. Осталось добавить НТМL-элемент, который вспомога­ тельная функция дескриптора будет обрабатывать для создания ссылок на страницы (листинг Листинг 8.29). 8.29. Добавление ссылок на страницы в файле из папки List. cshtml Views/Product @model ProductsListViewModel @foreach (var р in Model.Products) <div> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c" )</h4> </div> <div page-model="@Model.Pagin ginfo" page-action="List"></div> Запустив приложение, вы увидите новые ссьшки на страницы (рис. 8.9). Стиль отображения довольно примитивен, но далее в главе он будет улучшен. Пока важно то, что ссылки позволяют перемещаться по страницам каталога и знакомиться с то­ варами, выставленными на продажу. Когда механизм page-model в элементе div, он обращается к классу Razor обнаруживает атрибут PageLinkTagHelper образования элемента, что и дает набор ссылок, показанных на рис. 8.9. для пре­ 240 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 D SponsStoro Ka~·ak А boat for ono p<rsoi1 Gold-plat<d, diamond-studdod King S275.00 S79,500.00 Sl,200.00 TЫnldng Сар ш Lifejacktt S4S.9S Slб.00 Socct1· BaU Uusttady Cbair ш Рис. 8.9. Отображение ссылок для навигации по страницам Почему бы просто не воспользоваться элементом управления GridView? Если вы ранее работали с ASP.NET, то вам может показаться, что такой большой объем ра­ боты привел к довольно скромным результатам. Пришлось написать немало кода лишь для того, чтобы получить список товаров с разбиением на страницы. Если бы мы прибегли к услугам инфраструктуры Web Forms, то такого же результата можно было бы достичь с при­ менением готового элемента управления привязав его напрямую к таблице GridView или Lis t View Produc t s базы данных. из ASP.NET Web Forms, То, что было сделано в главе, вы глядит не слишком впечатляюще, но серьезно отличается от процесса перетаскивания какого-то элемента управления на поверхность визуального проектирования. Во-первых, мы строим приложение с надежной и удобной в сопровожде­ нии архитектурой, которая обеспечивает надлежащее разделение обязанностей. В отличие от простейшего использования Li s t v i ew мы не связываем напрямую пользовательский интерфейс и базу данных, что представляет собой подход, который дает быстрые резуль­ таты, но вызовет проблемы и сложности в будущем . Во-вторых, в ходе дела мы создаем модульные тесты, что позволяет проверять поведение приложения естественным образом, который практически невозможен в случае применения сложного элемента управления Web Forms. В-третьих, не забывайте, что значительная часть главы посвящена созданию инфра­ структуры, на основе которой строится приложение. Например, определить и реализовать хранилище нужно только один раз, и теперь, когда оно имеется в нашем распоряжении, новые функциональные средства можно строить и тестировать легко и быстро, как будет продемонстрировано в последующих главах. Глава 8. SportsStore: реальное nриложение 241 Разумеется, ничто из сказанного отнюдь не умаляет значимость безотлагательных результа­ тов, которые способна предложить инфраструктура Web Forms, но, как объяснялось в главе 3, такая безотлагательность имеет свою цену, которая может оказаться высокой и болезнен­ ной в крупных и сложных проектах. Улучшение URL Ссьmки на страницы работают, но для передачи информации серверу они по-пре­ жнему используют строку запроса следующего вида: http://localhost/?productPage=2 Чтобы получить более привлекательные следует шаблону компонуемых URL. URL, необходимо создать схему, которая Компонуемым называется URL. имеющий смысл для пользователя, такой как показанный ниже: http://localhost/Page2 Инфраструктура МVС позволяет легко изменять схему URL в приложении, потому что применяет средство маршрутизацииАSР.NЕТ, которое отвечает за обработку URL для выяснения, на какую часть приложения они указывают. Понадобится лишь доба­ вить новый маршрут при регистрации промежуточного программного обеспечения MVC в методе Листинг 8.30. Configure () Startup (листинг Добавление нового маршрута в файле из папки using using using using using using using using using using using класса 8.30). Startup. cs SportsStore System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; SportsStore.Models; Microsoft.Extensions.Configuration; Microsoft.EntityFrameworkCore; namespace SportsStore { puЫic class Startup { Startup(IConfiguration configuration) => Configuration = configuration; puЫic puЫic IConf iguration Configuration { get; } void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(optio ns => options.UseSqlServer( Configuration["Data:SportStoreProducts:Connectio nString"]) ); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddMvc(); puЫic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); puЫic арр, IHostingEnvironment env) { 242 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 a pp.U seSta tus Code Pages() ; app.U seS t a ticFile s ( ) ; app.UseM vc (routes => { routes.MapRoute( name: "pagination", template: "Products/Page{productPage}" , defaults: new ( Controller = "Product", action = "List" }) ; rou tes .MapRout e( name: "default", temp lat e: " { c o n tro ller=Pr o d uc t} / { act ion= List} / { i d ?} " ) ; ) ) ; Se ed Da t a . Ensu r ePopulate d (app); Новый маршрут важно поместить перед стандартным маршрутом (по имени d e f au l t) . который уже присутствует в методе . Как будет показано в главе 15. систе ма маршрутизации обрабатывает маршруты в порядке их перечисления. а нам нужно. чтобы новый маршрут имел преимущество перед существующим маршрутом. Это единственное изменение. которое требуется внести. чтобы изменить сх е му URL для разбиения на страницы списка товаров . Инфраструктура МVС тесно интег­ рирована с функцией маршрутизации . а потому приложение автоматически отраж ает изменение подобного рода в URL, используемых приложением. включая URL. которые генерируются вспомогательными функциями дескрипторов вроде применяемых для генерации ссылок на страницы. Не беспокойтесь. если маршрутизация пока не осо­ бенно понятн а - она будет подробно ра с сматриваться в главах 15 и 16. Запустив приложени е и щелкнув на ссылке для какой-нибудь страницы. можно наблюдать новую схему С ф URL в действии (рис. 8 . 10). localhost .53406/Product•/Page2 Stadium Flat-pack~d 35 ,000-s~a1 stad1um $79,500.00 Рис. 8.10. Новая схема URL, отображаемая в браузере Стилизация содержимого Мы построили значительную часть инфраструктуры и базовые средства приложе ­ ния и готовы к сборке всего воедино, но пока совершенно не уделяли внимание его внешнему виду . Хотя данная книга не посвящена веб-ди з айну или CSS , внешний вид приложения SpoгtsStore настолько примитивен , что умаляет его технические достоин с ­ тва . В настоящем разделе мы постараемся исправить ситуацию . Мы собираемся р еали­ зовать классическую компоновку. содержащую два столбца и заголовок (рис. 8 . 11 ). Глава 8. SportsSto re: реальное приложение Home • Watersports • Soccer • Chess 243 • Product 1 • Product 2 • (main body) • Рис. Установка пакета 8.11. Целевой дизайн для приложения SportsStore Bootstrap Для предоставления стилей CSS, которые будут применены к приложению, мы пла­ Bootstrap. При установке пакета Bootstrap мы будем пола­ гаться на встроенную в Visual Studio поддержку инструмента Bower. Выберем шаблон элемента Bower Configuration File (Файл конфигурации Bower) из категории General в диалоговом окне Add New ltem, чтобы создать в проекте SportsStore файл по име­ ни bower. j son. как демонстрировалось в главе 6. Добавим в раздел depe ndenci es созданного файла пакет Bootstrap, как показано в листинге 8.31. Ранее уже упомина­ лось, что в примерах книги применяется предварительный выпуск Bootstrap. нируем использовать пакет Листинг 8.31. Добавление пакета Bootstrap в файле bower. j son из папки SportsStore "name": "asp.net ", "private": t r ue, " de pendencies": { "bootstrap": "4 . 0.0-alpha.6" После сохранения изменений, внесенных в файл bowe r . j son, с реда Visual Studio Bower загрузит пакет Bootstrap в папку wwwroo t / 1 i Ь/ Bootstrap зависит от пaкeтajQuery, который также автоматически с помощью инструмента boo tstrap. Пакет добавится в проект. Применение стилей В главе 5 Bootstrap к компоновке объяснялось. как работают представления Razor, как они используются и как встраивать в них компоновки. Файл запуска представления, добавленный в нача­ ле главы, указывает, что в качестве стандартной компоновки должен выступать файл по имени Layout .cshtml, именно к нему (листинг так что начальную стилизацию 8.32). Bootstrap мы применим 244 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 8.32. Применение СSS-файла из папки Bootstrap к фaйлy _Layout.cshtml Views/Shared <IDOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" asp-href-include="/lib/bootstrap/dist/**/*.min.csв" asp-href-exclude="**/*-reboot*,**/*-grid*" /> <title>SportsStore</title> </head> <body> <div clasв="navbar navbar-inverвe bg-inverse" role="navigation"> <а class"'"navbar-brand" href"'"*">SPORTS STORE</a> </div> <div class="row m-1 р-1"> <div id="categories" clasв,,,"col-3"> Put something useful here later </div> <div class•"col-9"> @RenderBody () </div> </div> </body> </html> Элемент link имеет атрибуты asp-href-include и asp-href-exclude, которые представляют собой пример использования встроенного класса вспомогательной фун­ кции дескриптора. В этом случае вспомогательная функция дескриптора просмат­ ривает значения атрибутов и генерирует элементы указанному атрибутом include, link для всех файлов по пути, и по путям, указанным атрибутами exclude. Пути в атрибутах могут содержать групповые символы, что делает такой прием удобным способом обеспечения возможности добавления и удаления файлов из структуры па­ пок wwwroot, не нарушая работу приложения. Но, как объясняется в главе 25, следует проявлять внимание, чтобы из указанных путей выбирались только те файлы, кото­ рые ожидается получить. Добавление в компоновку таблицы стилей CSS из Bootstrap означает. что опреде­ ленные в ней стили можно применять в любом представлении, которое полагается на данную компоновку. В листинге Листинг 8.33. 8.33 стилизация используется в файле Стилизация содержимого в файле из папки List. cshtml. List.cshtml Views/Product @model ProductsListViewModel @foreach (var р in Model.Products) <div claвs,,,"card card-outline-primary m-1 р-1"> <div class,,,"bg-faded р-1"> <h4> @p.Name <span class="badge badge-pill badge-primary" style,,,"float: right"> Глава 8. SportsStore: реальное приложение 245 <small>@p.Price.ToString("c" )</small> </span> </h4> </div> <di v class=" card-text р-1 ">@р. Description</ di v> </div> ) <div page-model="@Model.Paginginf o" page-action="List" page-classes-enaЫed="true" page-class="Ьtn" page-class-normal="Ьtn-secondary" page-class-selected="Ьtn-primary" class="Ьtn-group pull-right m-1"> </div> Нам необходимо стилизовать кнопки, которые генерирует класс но жестко встраивать классы Bootstrap PageLinkTagHelper, в код нежелательно, т.к. в итоге затруднит­ ся повторное использование вспомогательной функции дескриптора где-то в другом месте приложения либо изменение внешнего вида кнопок. Взамен мы определяем в элементе di v специальные атрибуты, которые указывают требуемые классы и соот­ ветствуют свойствам, добавленным во вспомогательную функцию дескриптора. Затем они применяются для стилизации создаваемых элементов а (листинг Листинг 8.34. Добавление классов к генерируемым элементам в файле PageLinkTagHelper. cs using using using using using using 8.34). из папки Infrastructure Microsoft.AspNetCore.Mvc ; Microsoft.AspNetCore.Mvc .Rendering; Microsoft.AspNetCore.Mvc .Routing; Microsoft.AspNetCore.Mvc .ViewFeatures; Microsoft.AspNetCore.Raz or.TagHelpers; SportsStore.Models.ViewM odels; namespace SportsStore.Infrastructur e { [HtmlTargetElement("div", Attributes = "page-model")] class PageLinkTagHelper : TagHelper { private IUrlHelperFactory urlHelperFactory; puЬlic PageLinkTagHelper(IUrlHe lperFactory helperFactory) urlHelperFactory = helperFactory; puЫic [ViewContext] [HtmlAttributeNotBound] puЬlic ViewContext ViewContext { get; set; puЫic Paginginfo PageModel { get; set; puЬlic string PageAction { get; set; puЫic puЫic puЫic puЫic = false; bool PageClassesEnaЫed ( get; set; string PageClass { get; set; } string PageClassNormal { get; set; 1 string PageClassSelected ( get; set; override void Process(TagHelperContext context, TagHelperOutput output) { IUrlHelper urlHelper = urlHelperFactory.GetUrlHe lper(ViewContext); TagBuilder result = new TagBuilder("div"); puЫic 246 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 for (int i = 1; i <= PageModel.TotalPages; i++) { TagBuilder tag = new TagBuilder("a"); tag.Attributes["href"] = urlHelper.Action(PageAction, new { productPage = i }) ; if (PageClassesEnaЬled) { tag.AddCssClass(PageClass); tag.AddCssClass(i == PageМodel.CurrentPage ? PageClassSelected : PageClassNormal) ; tag.InnerHtml.Append(i.ToString()); result.InnerHtml.AppendHtml(t ag) ; output.Content.AppendHtml(result.InnerHtml); Значения атрибутов автоматически используются для установки значений свойств во вспомогательной функции дескриптора, учитывая отображение между форматом имени атрибута С# (PageClassNormal). HTML (page-class-normal) и форматом имени свойства Это позволяет вспомогательным функциям дескрипторов по­ разному реагировать в зависимости от атрибутов НТМL-элемента, обеспечивая более гибкий способ генерации содержимого в приложении МVС. Запустив приложение, легко заметить , что его внешний вид улучшился ком случае. в какой-то степени (рис. - во вся­ 8. 12). D SportsS1or~ useful here later Bling-Bling Кing Gold-plated, diamond-studded King ~---- - - - - - - - - ----------- - Рис. 8.12. Приложение -------~ SportsStore с улучшенным дизайном Создание частичного представления В качестве завершающего штриха в главе мы проведем рефакторинг приложения, чтобы упростить представление List. cshtml. Мы создадим частичное представле­ ние, являющееся фрагментом содержимого, которое можно внедрять в другое пред­ ставление подобно шаблону. Частичные представления подробно рассматриваются в главе 21. Они помогают сократить дублирование, когда одно и то же содержимое Глава 8. SportsStore: реальное приложение 247 должно появляться в разных местах приложения. Вместо того чтобы копировать и вставлять одинаковую разметку Razor во множество представлений, можно опре­ делить ее единожды в частичном представлении. Чтобы создать частичное пред­ ставление, добавим в папку ProductSummary. cshtml Листинг 8.35. Views/Shared файл представления Razor по имени и поместим в него разметку, показанную в листинге Содержимое файла ProductSuпunary. cshtml из папки 8.35. Views/Shared @model Product <div class="card card-outline-primary m-1 р-1"> <div class="bg-faded р-1"> <h4> @Model.Name <span class="badge badge-pill badge-primary" style="float:right"> <small>@Model.Price.ToString("c")</small> </span> </h4> </div> <div class="card-text p-l">@Model.Description</div> </div> Теперь необходимо модифицировать файл List. cshtml из папки чтобы в нем применялось частичное представление (листинг Листинг 8.36. Использование частичного представления в файле из папки Views/Product, 8.36). List.cshtml Views/Product @model ProductsListViewModel @foreach (var р in Model.Products) @Htm1.Partial("ProductSummary", р) <div page-model="@Model.Paginglnfo" page-action="List" page-classesenaЫed="true" page-class="btn" page-class-normal="btn-secondary" page-class-selected="btn-primary" class="btn-group pull-right m-1"> </div> Мы взяли код разметки, который ранее размещался в цикле представления List. cshtml, foreach внутри и перенесли его в новое частичное представление. Обращение к частичному представлению производится с помощью вспомогательного метода Html. Partial (), которому в аргументах передаются имя представления и объект модели представления. Подобного рода переход на частичное представление является рекомендуемым приемом, поскольку он позволяет вставлять одну и ту же разметку в любое представление, которое нуждается в отображении сводки о товаре. Как видно на рис. 8.13, добавление частичного представления не изменяет внешний вид приложения; оно просто меняет место, где механизм Razor находит содержимое, которое применяет для генерации ответа, отправляемого браузеру. 248 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 useful here later Kayak А boat for one person Lifejacket .,,,,. Proteetive and fashionaЫe Рис. 8.13. Использование частичного представления Резюме В главе была построена основная инфраструктура для приложения SportsStore. Пока что она не содержит достаточного набора функциональных средств. чтобы их можно было продемонстрировать пользователю прямо сейчас, но "за кулисами" уже имеются зачатки модели предметной области с хранилищем товаров. которое подде­ рживается SQL Server и Entity Framework Core. В приложении присутствует единс­ Product. который может создавать разбитые на страницы спис­ товаров. а также настроена ясная и дружественная к пользователям схема URL. твенный контроллер ки В настоящей главе могло показаться. что для получения весьма незначительных выгод пришлось провести излишне трудоемкую начальную подготовку. но в следу­ ющей главе равновесие будет восстановлено. Теперь , когда фундаментальная струк­ тура на месте. можно двигаться дальше и добавлять все средства, ориентированные на пользователя: навигацию по категориям, корзину для покупок и начальную форму для оплаты. ГЛАВА 9 навигация SportsStore: в настоящей главе мы продолжим построение примера приложения SportsStore, добавив к нему поддержку навигации по приложению и начав создавать корзи­ ну для покупок. Добавление навигационных элементов управления Приложение SportsStore станет более удобным, если пользователи получат возмож­ ность навигации среди товаров по категории. Мы будем решать задачу в три этапа. • Расширим метод действия List () в классе ProductController, Product в хранилище. чтобы он был способен фильтровать объекты URL. • Пересмотрим и расширим схему • Создадим список категорий, который будет находиться в боковой панели сай­ подсвечивая та, текущую категорию и предоставляя ссылки на остальные категории. Фильтрация списка товаров Мы начнем с расширения класса модели представления который бьш добавлен в проект SportsStore ProductsListViewModel, в предыдущей главе. Нам нужно обес­ печить взаимодействие текушей категории с представлением, чтобы визуализировать боковую панель, и это хорошая отправная точка. В листинге внесенные в файл Листинг 9.1. ProductsListViewModel. cs Добавление свойства в файле из папки из папки 9.1 ProductsListViewModel. cs Models/ViewModels using System.Collections.Gener ic; using SportsStore.Models; namespace SportsStore.Models.ViewM odels class ProductsListViewModel { get; set; } Products puЫic Paginginfo Paginginfo { get; set; puЫic string CurrentCategory { get; set; } puЫic puЫic IEnumeraЬle<Product> } показаны изменения, Models/ViewModels. 250 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 В масс ProductsListViewModel добавлено свойство по имени Следующий шаг замючается в обновлении масса действия List () фильтровал объекты Product CurrentCategory. ProductController, чтобы метод по категории и использовал только что добавленное в модель представления свойство для указания категории, выбран­ ной в текущий момент. Изменения приведены в листинге Листинг 9.2. Добавление поддержки категорий к методу действия в файле using using using using 9.2. ProductController. cs из папки List () Controllers Microsoft.AspNetCore.Mvc; SportsStore.Models; System.Linq; SportsStore.Models.ViewModels; namespace SportsStore.Controllers { class ProductController : Controller private IProductRepository repository; puЫic int PageSize = 4; puЬlic ProductController(IProductRepository repo) { repository = repo; puЫic ViewResult List(string category, int productPage = 1) => View(new ProductsListViewModel { Products = repository.Products .Where(p => category = null 11 p.Category = category) .OrderBy(p => p.ProductID) .Skip( (productPage - 1) * PageSize) .Take(PageSize), Paginginfo = new Paginginfo { CurrentPage = productPage, ItemsPerPage = PageSize, Totalitems = repository.Products.Count() puЬlic ) , CurrentCategory = category ) ) ; В метод действия List () внесены три изменения. Первое - добавлен новый category. Он применяется вторым изменением, которое свя­ зано с расширением запроса LINQ. Если значение category не равно null, тогда выбираются только объекты Product с соответствующим значением в свойстве Category. Последнее, третье, изменение касается установки значения свойства CurrentCategory, которое было добавлено в класс ProductsListViewModel. Однако в результате таких изменений значение Paginginfo. Totalitems вычисля­ параметр по имени ется некорректно, потому что оно не принимает во внимание фильтр по категории. Со временем мы все исправим. Глава 9. SportsStore: навигация 251 Модульное тестирование: обновление существующих модульных тестов Мы изменили сигнатуру метода действия List (), поэтому некоторые существующие ме­ тоды модульного тестирования перестали компилироваться. Чтобы решить возникшую про­ блему, в модульных тестах, которые работают с контроллером, методу действия List () null. Например, раздел действия ProductControllerTests. cs должен понадобится передавать в первом параметре значение тестового метода Can_Paginate () в файле выглядеть следующим образом: [Fact] puЫic 11 void Can_Paginate () Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] "Pl"), 1, Name new Product {ProductID "Р2"), 2, Name new Product IProductID "Р3"), 3, Name new Product {ProductID "Р4"}, 4, Name new Product {ProductID 5, Name "Р5"} new Product {ProductID }) .AsQueryaЬle<Product>()); ProductController controller controller.PageSize = 3; 11 new ProductController(mock.Object); Действие ProductsListViewModel result controller.List(null, 2) .ViewData.Model as 11 ProductsListViewМodel; Утверждение Product[] prodArray = result.Products.ToArray(); Assert.True(prodArray.Length == 2); Assert. Equal ( "Р4", prodArray [О] • Name) ; Assert.Equal("P5", prodArray[l] .Name); Указывая null для category, мы получаем все объекты Product, которые конт­ роллер извлекает из хранилища, что воспроизводит ситуацию перед добавлением но­ вого параметра. Такого же рода изменение необходимо внести в тестовый метод Can_Send_Pagination_View_Model(): [Fact] puЫic 11 void Can_Send_Pagination_View_Model() 1 Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => rn.Products) .Returns( (new Product[] 1 "Pl"}, new Product {ProductID = 1, Narne "Р2"}, 2, Narne new Product {ProductID "Р3"}, 3, Narne new Product {ProductID "Р4"}, 4, Narne new Product {ProductID "Р5"} 5, Narne new Product {ProductID }) .AsQueryaЬle<Product>()); 11 Организация ProductController controller new ProductController(rnock.Object) ( PageSize 3 }; 252 11 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Действие ProductsListViewModel result = controller.List(null, 2) .ViewData.Model as 11 ProductsListViewМodel; Утверждение Paginginfo pageinfo = result.Paginginfo; Assert.Equal ( 2, pageinf o .CurrentPage ) ; Assert.Equal(З, pageinfo.ItemsPerPage); Assert.Equal(5, pageinfo.Totalitems); Assert.Equal(2, pageinfo.TotalPages); Когда вы примете образ мышления, ориентированный на тестирование, поддержание мо­ дульных тестов в синхронизированном состоянии с изменениями кода быстро станет вашей второй натурой. Чтобы увидеть результат фильтрации по категории. запустим приложение и выбе­ рем категорию с помощью показанной ниже строки запроса (замените номер порта тем. который был назначен вашему проекту средой том, что Soccer начинается с прописной буквы Visual Studio). позаботившись о S: http://localhost:60000/?category=Soccer Отобразятся только товары из категории Soccer (рис. 9.1). Вряд ли пользователи захотят переходить по категориям с применением здесь было показано, что незначительные изменения в приложении MVC зывать крупное влияние, если базовая структура на месте. Put something useful here later Soccer Ball FIFA-approved size and weight - ~ - -- Corner Flags Give your playing field а professional toucl1 - -- Clll~·!·l·!·t Stadium Flat-packed 35.000-seat stadium 11 Рис. 9.1. 2 3 Использование строки запроса для фильтрации по категории URL. но могут ок<~­ Глава 9. SportsStore: навигация 253 Модульное тестирование: фильтрация по категории Нам необходим модульный тест для проверки функциональности фильтрации по кате­ гории, чтобы удостовериться в том, что фильтр может корректно генерировать сведения о товарах указанной категории. Ниже приведен тестовый метод, добавленный в класс ProductControllerTests: [Fact] void Can Filter Products() { - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] { "Pl", Category = "Catl"}, new Product {ProductID = 1, Name "Р2", Category = "Cat2"}, new Product { ProductID = 2, Name "РЗ", Category = "Catl"}, new Product {ProductID = 3, Name "Р4", Category = "Cat2"}, new Product { ProductID = 4, Name "PS", Category = "СаtЗ"} new Product { ProductID = 5, Name }) .AsQueryaЫe<Product>()); puЫic 11 Организация 11 11 Организация - создание контроллера и установка размера страницы в три элемента ProductController controller = new ProductController(mock.Object); controller.PageSize = З; 11 Действие Product[] result = (controller.List("Cat2", 1) .ViewData.Model as ProductsListViewModel) .Products.ToArray(); 11 Утверждение Assert.Equa1(2, result.Length); "Р2" && result[O] .Category Assert.True(result[O] .Name Assert.True(result[l] .Name == "Р4" && result[l] .Category Тест создает имитированное хранилище, содержащее объекты "Cat2"); "Cat2"}; Product, которые отно­ List () запрашивается сятся к диапазону категорий. С использованием метода действия одна специфическая категория, и результаты проверяются на предмет наличия корректных объектов в правильном порядке. Улучшение схемы URL Мало кому понравится /?category=Soccer. маршрутизации в методе ный набор URL видеть либо иметь дело с неуклюжими URL вроде Для решения этой проблемы мы намерены изменить схему (листинг Configure () класса Startup, чтобы создать более удоб­ 9.3). Внимание! Новые маршруты в листинге 9.3 важно добавлять в показанном порядке. Маршруты применяются в порядке, в котором они определены, поэтому изменение по­ рядка может привести к нежелательным эффектам. Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 254 Листинг 9.3. puЬlic void Configure(IApplicationBuilder Изменение схемы маршрутизации в файле Startup. cs арр, из папки SportsStore IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.мapRoute( null, tem.plate: 11 {cate9ory}/Pa9e{productPa9e:int} 11 , defaults: new { controller = "Product", action naпie: = "List" } ) ; routes.мapRoute( naпie: null, 11 Pa9e{productPa9e:int} 11 , defaults: new { controller = 11 Product", action = 11 List", productPage = 1 } teшplate: ) ; routes.мapRoute( null, tem.plate: 11 {cate9ory} 11 , defaults: new { controller = 11 Product 11 , action = 11 List 11 , productPage = 1 } naпie: ) ; routes.мapRoute( null, : 11 11 , defaults: new { controller productPage = 1 }) ; nаше: teшpla te routes.мapRoute(naпie: )) ; null, = "Product", teшplate: action = "List", "{controller}/{action}/{id?}"); SeedData.EnsurePopulated(app); В табл. 9.1 описана схема URL, которую представляют определенные маршруты. Система маршрутизации подробно объясняется в главах Таблица 9.1. 15 и 16. Сводка по маршрутам URL Что делает ! Выводит первую страницу списка товаров всех категорий /Page2 Выводит указанную страницу (в данном случае страницу 2), отображая товары всех категорий /Soccer /Soccer/Page2 Выводит первую страницу товаров указанной категории Выводит указанную страницу (страницу категории (Soccer) 2) (Soccer) товаров заданной Глава 9. SportsStore: навигация Система маршрутизации ASP.NET используется инфраструктурой MVC для обра­ ботки входящих запросов от пользователей, но также генерирует исходящие которые соответствуют схеме URL 255 URL, и потому могут встраиваться в веб-страницы. Применение системы маршрутизации для обработки входящих запросов и генерации исходящих URL позволяет Интерфейс URL. гарантировать согласованность всех IUrlHelper URL в приложении. предоставляет доступ к функциональности генерации Мы использовали этот интерфейс и определяемый им метод Action () в классе вспомогательной функции дескриптора. созданном в предыдущей главе. Теперь, когда нужно генерировать более сложные URL, необходим способ получения дополнитель­ ной информации от представления, не добавляя дополнительные свойства к классу вспомогательной функции дескриптора. К счастью, классы вспомогательных функ­ ций дескрипторов обладают удобным средством, которое позволяет получать в одной коллекции все свойства с общим префиксом (листинг Листинг 9.4. Получение значений атрибутов, снабженных префиксом, в файле using using using using using using 9.4). PageLinkTagHelper. cs из папки Infrastructure Microsoft.AspNetCore.Mvc; Microsoft.AspNetCore.Mvc.Rendering; Microsoft.AspNetCore.Mvc.Routing; Microsoft.AspNetCore.Mvc.ViewFeatures; Microsoft.AspNetCore.Razor.TagHelpers; SportsStore.Models.ViewModels; using System.Collections.Generic; namespace SportsStore.Infrastructure [HtmlTargetElement("div", Attributes = "page-model")] class PageLinkTagHelper : TagHelper { private IUrlHelperFactory urlHelperFactory; puЬlic PageLinkTagHelper(IUrlHelperFactory helperFactory) urlHelperFactory = helperFactory; puЫic [ViewContext] [HtmlAttributeNotBound] puЫic ViewContext ViewContext { get; set; puЫic Paginginfo PageModel { get; set; } puЫic string PageAction { get; set; } [HtmlAttributeName(DictionaryAttributePrefix = "page-url-")] Dictionary<string, object> PageUrlValues { get; set; } = new Dictionary<string, object>(); puЫic puЫic puЫic puЫic puЫic bool PageClassesEnaЫed { get; set; } string PageClass { get; set; } string PageClassNormal { get; set; } string PageClassSelected { get; set; = false; override void Process(TagHelperContext context, 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"); puЫic PageUrlValues [ "productPage"] = i; tag.AttriЬutes["href"] = urlHelper.Action(PageAction, PageUrlValues); 256 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 if (PageClassesEnaЫed) { tag.AddCssClass(PageClass); tag.AddCssClass(i == PageModel.CurrentPage ? PageClassSelected : PageClassNormal); tag.InnerHtml.Append(i.ToString()); result.InnerHtml.AppendHtml(tag); output.Content.AppendHtml(result.InnerHtml); Декорирование свойства в классе вспомогательной функции дескриптора посредс­ твом атрибута HtmlAttributeName позволяет указывать префикс для имен атрибу­ тов элемента. которым в данном случае будет page-ur 1-. Значение любого атрибута, чье имя начинается с такого префикса. будет добавлено в словарь, присваиваемый свойству PageUr l Val ues, чтобы сгенерировать URL которое затем передается методу для атрибута href IUr lHelper. Action () , элементов а, выпускаемых вспомога­ тельной функцией дескриптора. В листинге 9.5 к элементу di v добавлен новый атрибут. который обрабатывается вспомогательной функцией дескриптора и указывает категорию, применяемую для генерации URL. В представление был добавлен только один атрибут, но в словарь до­ бавился бы любой атрибут с тем же самым префиксом. Листинг 9.5. Добавление нового атрибута в файле List. cshtml из папки Views/Home @model ProductsListViewModel @foreach (var р in Model.Products) @Html.Partial("ProductSummary", р) <div page-model="@Model.Paginglnfo" page-action="List" page-classes-enaЫed="true" page-class="btn" page-class-normal="btn-secondary" page-class-selected="btn-priшary" page-url-category="@Мodel.CurrentCategory" class="btn-group pull-right m-1"> </div> До внесения этого изменения ссьтки для перехода на страницы генерировались так: http://<cepвep>:<пopт>/Pagel Если пользователь щелкнет на страничной ссылке подобного типа, то фильтр по категории утрачивается и приложение отобразит страницу. содержащую товары всех категорий. За счет добавления текущей категории. получаемой из модели представле­ ния. взамен генерируются URL следующего вида: http://<cepвep>:<пopт>/Chess/Pagel Когда пользователь щелкает на ссылке такого рода. текущая категория передается методу действия List () ния можно посещать и фильтрация сохраняется. После внесения данного измене­ URL вроде /Chess или /Soccer и наблюдать, что страничные ссылки, расположенные внизу. корректно включают категорию. Глава 9. SportsStore: навигация 257 Построение меню навигации по категориям Нам нужно предложить пользователям способ выбора категории, который не пре­ цусматривает ввод URL, что означает необходимость в отображении списка доступ­ ных категорий с отмеченной текущей категорией, если она есть. По мере построения приложения список категорий будет задействован в нескольких контроллерах, а по­ тому он должен быть самодостаточным и многократно используемым. В инфраструктуре ASP.NET Core MVC поддерживается концепция компонентов представлений, которые идеально подходят для создания единиц вроде многократно используемого навигационного элемента управления. Компонент представления - это класс С#, который предлагает небольшой объем многократно используемой при­ кладной логики с возможностью выбора и отображения частичных представлений Razor. Компоненты представлений подробно рассматриваются в главе 22. В данном случае мы создадим компонент представления, который визуализиру­ ет навигационное меню и интегрирует его в приложение за счет обращения к этому компоненту из разделяемой компоновки. Подход подобного рода дает нам обычный класс С#, который может содержать любую необходимую прикладную логику и ко­ торый можно подвергать моцульному тестированию подобно любому другому классу. Такой способ удобен при создании небольших сегментов приложения с одновремен­ ным сохранением общего подхода MVC. Создание навигационного компонента представления Создадим папку по имени Components, которая по соглашению является местом хранения компонентов представлений, и добавим в нее файл класса под названием NavigationMenuViewComponent. cs Листинг 9.6. с определением, показанным в листинге 9.6. Содержимое файла NavigationМenuViewComponent.cs из папки Componen ts using Microsoft.AspNetCore.Mvc; namespace SportsStore.Components puЫic class NavigationMenuViewComponent : ViewComponent string Invoke() { return "Hello from the Nav View Component"; puЬlic Метод Invoke () компонента представления вызывается, когда компонент приме­ няется в представлении Razor, а результат, возвращаемый методом Invoke (),встав­ ляется в НТМL-разметку, отправляемую браузеру. Мы начали с простого компонента представления, который возвращает строку, но вскоре заменим его динамическим НТМL-содержимым. Список категорий должен присутствовать на всех страницах, поэтому мы собира­ емся использовать компонент представления в разделяемой компоновке, а не в отде­ льном представлении. Внутри компоновки компоненты представлений применяются через выражение @await Component. InvokeAsync (),как показано в листинге 9.7. 258 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 9.7. Использование компонента представления в фaйлe из папки _Layout.cshtml Views/Shared < 1 DOCTYPE h tm l > <html> <hea d > <me ta n ame= " v iewp o r t " cont ent=" width=dev ice - width " /> <link r e l = " st ylesheet" asp- href- includ e= " / liЬ/boo ts t ra p/dist/ * * / *.min.cs s " asp- href -ex cl u de= "* */ *-reb oot• , ••/ •- grid* " /> <t i tl e >SportsS t o r e</tit l e> </ h e ad > <bo dy > <div cla ss= "navbar n avba r-inver s e bg- inver se " ro le="na vigatio n"> <а cla ss= "n a vbar-br a nd" href = "#" >SP ORTS ST ORE</ a> </ div> <d i v c l ass= " row m-1 р - 1 " > <d i v id= " catego r ies " c l a s s =" col - 3 " > @awai t Componen t. InvokeAsync ( "Naviga tionМenu" ) </div> <div c lass= "col-9" > @RenderBody () </ d iv> </div> </b ody> </ html> Текст заполнителя зам ен е н вызовом метода Component . I nvokeAsync (). Аргумен­ ViewComp onen t. т.е. с п о мо­ щью Navigat i onMenu ука з ывается клас с Navi gati onMen uView Component . Запу стив приложение, можно заметить. что вывод и з метода InvokeAsync ( ) включен в НТМL ­ том метода является имя класса компон е нта без части разметку. отправляемую браузеру (рис . 9.2). SportsStore Hello from the Thinking Nav View Cornponeпt Сар lmprove brain efficieпcy Ьу 75% Unsteady Chair Рис. 9.2. Применение компонента представления •+4t• Глава 9. SportsStore: навигация 259 Генерация списков категорий Теперь можно возвратиться к навигационному компоненту представления и сге­ нерировать реальный набор категорий. Построить НТМL-разметку для категорий можно было бы программно, как делалось во вспомогательной функции дескриптора для страничных ссылок, но одно из преимуществ работы с компонентами представ­ лений заключается в том, что они могут визуализировать частичные представления Razoг. Это означает, что компонент представления можно использовать для генерации списка категорий и затем применить более выразительный синтаксис Razoг для ви­ зуализации НТМL-разметки, которая отобразит данный список. Первым делом нужно обновить компонент представления, как показано в листинге Листинг 9.8. 9.8. Добавление категорий в файле NavigationМenuViewComponent. cs из папки Componen ts using Microsoft.AspNetCore.Mvc ; using System.Linq; using SportsStore.Models; namespace SportsStore.Components class NavigationMenuViewCompon ent : ViewComponent { private IProductRepository repository; puЫic puЫic NavigationМenuViewComponent(IProductRepository repo) repository = repo; IViewComponentResul t Invoke () return View(repository.Products .Select(x => x.Category) . Distinct () .OrderBy(x => х)); puЬlic Конструктор, определенный IProductRepository. в листинге Когда инфраструктуре 9.8, принимает аргумент типа MVC необходимо создать экземпляр класса компонента представления, она отметит потребность в предоставлении это­ го аргумента и просмотрит конфигурацию в классе Startup, чтобы выяснить, какой объект реализации должен использоваться. Мы имеем дело с тем же самым средством внедрения зависимостей, которое применялось в контроллере из главы будет аналогичным - 8, и результат обеспечение компоненту представления доступа к данным без необходимости знать используемую реализацию хранилища, как описано в главе В методе Invoke () с помощью LINQ 18. выбирается и упорядочивается набор кате­ горий в хранилище, после чего он передается в качестве аргумента методу View (), который визуализирует стандартное частичное представление Razoг. Детали час­ тичного представления возвращаются из метода с применением объекта реализа­ ции IViewComponentResul t главе 22). (данный процесс будет подробно рассматриваться в 260 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Модульное тестирование: генерация списка категорий Модульный тест, предназначенный для проверки возможности генерации списка категорий, относительно прост. Цель заключается в том, чтобы создать список, который отсортиро­ ван в алфавитном порядке и не содержит дубликатов. Проще всего это сделать, построив тестовые данные, которые имеют дублированные категории и не отсортированы должным образом, передав их вспомогательной функции дескриптора и установив утверждение, что данные были соответствующим образом очищены. Вот модульный тест, который определя­ ется в новом файле класса по имени NavigationMenuViewComponentTests. cs SportsStore. Tests: System.Collections.Generic; System.Linq; Microsoft.AspNetCore.Mvc.ViewComponents; Moq; SportsStore.Components; SportsStore.Models; Xunit; внут­ ри проекта using using using using using using using namespace SportsStore.Tests puЫic class NavigationMenuViewComponentTests [Fact] void Can_Select Categories() { 11 Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[J { new Product {ProductID 1, Name "Pl", Category = "Apples"), new Product {ProductID 2, Name "Р2", Category = "Apples"), new Product {ProductID 3, Name "РЗ", Category = "Plums"), new Product {ProductID 4, Name "Р4", Category = "Oranges"), )) .AsQueryaЫe<Product>()); puЫic NavigationMenuViewComponent target = new NavigationMenuViewComponent(mock.Object); 11 Действие - получение набора категорий string[J results = ( (IEnumeraЫe<string>) (target.Invoke() as ViewViewComponentResult) .ViewData.Model) .ToArray(); 11 Утверждение Assert. True (EnumeraЬle. SequenceEqual (new string [ J { "Apples", "Oranges", "Plums" ) , results)); Мы создаем имитированную реализацию хранилища, которая содержит повторяющиеся и несортированные категории. Затем мы устанавливаем утверждение о том, что дубликаты удалены и восстановлен алфавитный порядок. Создание представления При работе с представлениями, которые выбираются компонентами представле­ ний, механизм Razor использует разнообразные соглашения. И стандартное имя пред­ ставления, и местоположения, в которых ищется представление. отличаются от тех, Глава 9. SportsStore : нави гация которые приняты для контроллеров . Создадим папку NavigationMenu Vi ews/S hared /Components / Defaul t . cshtml с и добавим в нее файл представления по имени содержимым, приведенным в листинге Листинг 261 9.9. 9.9. Содержимое файла Default.cshtml из папки Views/Shared/ Components/NavigationМenu @model <а IEnumeraЫe<string > class= "btn btn-Ьlock btn-secondary " asp-action=" Li st" asp - controller="Pr oduct " as p-route-category= "" > Ноте </а> @f o rea ch (string c a teg ory in Mode 1) { <а c l ass =" Ьtn Ьt n - Ьl oc k Ьtn - se co nd ary " asp - action= " Lis t " asp-contro ller="Prod uc t " a sp-route - c a t egory="@category " asp-route-p rod uctPage = "l" > @category </а> В представлении применяется один из встроенных классов вспомогательных фун1щий дескрипторов (описанных в главах hre f которых содержит URL. 24 и 25) для создания элементов а, атрибут выбирающий определенную категорию товаров. После запуска приложения появятся сс ылки на категории (рис . 9.3) . Щелчок н а какой-то категории приводит 1< тому . что с писо к товаров обновляется, отображая только товары выбранной к атегор ии . Home Soccer Ball Chess FIFA-approved size and weigl1t Рис. 9.З. Генерация ссылок на категории с помощью ком понента представления 262 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 подсветка текущей категории В настоящий момент пользователь не располагает какой-нибудь визуальной под­ сказкой о выбранной категории. Хотя, основываясь на товарах в списке, можно вы­ двинуть предположение относительно категории, гораздо лучше предоставить более наглядную визуальную обратную связь. Компоненты ASP.NET Core MVC, такие как контроллеры и компоненты представлений, могут получать информацию о текушем запросе, обращаясь к объекту контекста. Большую часть времени заботу о получении объекта контекста можно поручить базовым классам, которые используются для со­ здания компонентов, подобно тому, как базовый класс Controller применяется для создания контроллеров. Базовый класс ViewComponent не является исключением и обеспечивает доступ к объектам контекста через набор свойств. Одно из свойств называется и предоставляет информацию о том, как URL RouteData запроса был обработан системой маршрутизации. В листинге 9.10 свойство RouteData используется для доступа к данным запро­ са, чтобы получить значение выбранной в текущий момент категории. Значение ка­ тегории можно было бы передать представлению путем создания еще одного класса модели представления (и так бы делалось в реальном проекте), но ради разнообразия применим объект Листинг 9.1 О. ViewBag, который был введен в главе 2. Передача выбранной категории в файле Naviga tionМenuViewComponen t. cs из папки Componen ts using Microsoft.AspNetCore.Mvc; using System.Linq; using SportsStore.Models; namespace SportsStore.Components class NavigationMenuViewComponent : ViewComponent { private IProductRepository repository; puЬlic NavigationMenuViewComponent(IProductRepository repo) repository = repo; puЬlic puЬlic IViewComponentResult Invoke() { ViewBag.SelectedCategory = RouteData?.Values["category"]; return View(repository.Products .Select(x => x.Category) . Distinct () .OrderBy(x => х)); Внутри метода в объекте ViewBag Invoke () мы динамически создаем свойство SelectedCategory и устанавливаем его значение равным значению текушей катего­ рии, которое получаем через объект контекста, возвращенный свойством Как объяснялось в главе 2, ViewBag RouteData. представляет собой динамический объект, кото­ рый позволяет определять новые свойства, просто присваивая им значения. Глава 9. SportsStore: навигация 263 Модульное тестирование: сообщение о выбранной категории Для выполнения проверки того, что компонент представления корректно добавил детали о выбранной категории, в модульном тесте можно прочитать значение свойства ViewBag, ViewViewComponentResul t, описанный в главе 22. Ниже показан модульный тест, добавленный в класс NavigatioMenuViewComponentTests: которое доступно через класс [F'act] puЬlic 11 void Indicates Selected Category() Организация string categoryToSelect "Apples"; Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] new Product {ProductID = 1, Name "Pl", Category "Apples"}, "Oranges"}, new Product {ProductID = 4, Name = "Р2", Category }) .AsQueryaЫe<Product>()); NavigationMenuViewComponent target = new NavigationMenuViewComponent(mock.Object); target.ViewComponentContext = new ViewComponentContext ViewContext = new ViewContext { RouteData = new RouteData() } }; target.RouteData.Values["category"] = categoryToSelect; 11 Действие string result = (string) (target. Invoke () as ViewViewComponentResul t). ViewData [ "SelectedCategory"); 11 Утверждение Assert.Equal(categoryToSelect, result); Модульный тест снабжает компонент представления данными маршрутизации через свойс­ тво ViewComponentContext, посредством которого компоненты представлений полу­ чают все свои данные контекста. Свойство ViewComponentContext обеспечивает до­ ступ к данным контекста, специфичным для представления, с помощью своего свойства ViewContext, которое в свою очередь обеспечивает доступ к информации о маршрути­ зации через собственное свойство RouteData. Большая часть кода в модульном тесте связана с созданием объектов контекста, которые будут предоставлять выбранную катего­ рию таким же способом, как она бы предлагалась во время выполнения приложения, когда данные контекста предоставляются инфраструктурой ASP.NET Core MVC. Теперь, когда доступна информация о том, какая категория выбрана, можно обно­ вить представление, выбираемое компонентом представления, чтобы задействовать эту информацию, и изменить классы CSS, которые используются для стилизации ссылок, сделав представление текущей категории отличающимся от остальных кате­ горий. В листинге 9.11 приведено изменение, внесенное в файл Defaul t. cshtml. 264 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Листинг 9.11. Подсветка текущей категории в файле Defaul t. cshtml из папки Views/Shared/Components/NavigationМenu @model <а IEnum era Ыe<string > c lass = "Ьt n btn-Ьlock Ьtn-secondary" asp -a ction = "List" asp-controller="Product" asp-route- ca tegory="" > Ноте </а> @foreach (stri ng category in Mode l) { <а class="Ьtn btn-Ыock @ (category = ViewВag.SelectedCategory? "btn-primary": "btn-secondary")" asp-action="List" asp-controller="Product" asp-route-category="@category" asp-route-productPage="l"> @ca tegory < /а> С помощью выражения Razoг внутри атрибута с 1а s s мы применяем класс Ьtn -prim ary к элементу, который представляет выбранную категорию. и класс Ьtn -second ary к остальным элементам. Указанные классы применяют разные стили Bootstrap и делают активную кнопку визуально отличающейся (рис . Home Thinking Chess lmprove brain efficiency Soccer Unsteady Chair fiQ.j,$ Сар Ьу 75% Secretly give your opponent а disadvantage Watersports Рис. 9.4. 9.4). Подсвечивание выбранной категории •* Глава 9. SportsStore: навигация 265 Корректировка счетчика страниц Мы должны скорректировать ссылки на страницы. чтобы они правильно работали , когда выбрана какая-то категория. В настоящий момент количество ссылок на стра­ ницы определяется общим числом товаров в хранилище, а не количеством товаров выбранной категории. Это значит. что пользователь может щелкнуть на ссылке для страницы 2 категории Chess и получить пустую страницу. поскольку товаров данной категории не хватает для заполнения второй страницы . Проблема демонстрируется на рис. 9.5. Chess Sоссег Watersports Рис. 9.5. Отображение некорректных ссылок на страницы , когда выбрана какая-то категория Проблему можно устранить. модифицировав метод действия ре Product мание (листинг Листинг 9.12. Создание данных о разбиении на страницы, учитывающих категории, ProductController. cs из папки Controllers Microsoft.As pNetCore.Mvc; SportsS to re.Models; System.Linq; SportsStore .Models.Vi e wMode ls; namespa ce SportsStore.Control ler s { c lass ProductControl l er : Controll e r private IPr oductReposit or y re pository; puЫic int PageSize = 4; puЫic ProductController {IProductReposit ory re po) { r epository = repo; p u Ыic в контролле­ 9. 12). в файле us ing usin g us i ng using Li s t () так. чтобы при разбиении на страницы категории принимались во вни­ 266 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 pu Ы ic ViewResu l t List(s t r i ng category , i nt productPage 1) => Vi ew(ne w Prod uctsL i s tViewMode l ( Produ cts = rep o s i t o ry.Produ cts . Wh e r e (р => c ategor y == n ul l 1 1 р. Ca tegor y ca tegor y ) . Ord e r By( p => p . Prod u ct I D) . S k ip( (p r oductPage - 1 ) * PageSize) . Ta ke(P age S iz e) , Pag i ngl nfo = ne w Pa gingiп fo { Cu rre ntPag e = productPag e , Ite ms Pe rPag e = Pa geSiz e , Total items = catego r y == null ? r e po s i t o r y . Produc ts. Count() re p o sit o ry. Pr odu cts.Where(e => e .Categ o ry = = categ ory) . Co unt() }' Current Catego r y = c ateg ory }) ; Если категория была выбрана, тогда возвращается количество позиций в ней. а если нет, то общее число товаров. Теперь во время просмотра товаров в какой-либо категории ссылки в нижн е й части страницы корректно отражают количество товаров в этой категории (рис. 9.6) . Home Thinking Chess lmprove brain efficiency Soccer Unsteady Chair Watersports Secretly give Сар уоuг Ьу 75% opponent а disadvantage Human Chess Board А fun game for the family Bling-Bling Кing " " - CjfJ.!Ф!.8 Gold-plated, diamond-studded King - 11 Рис. 9.6. Отображение ссылок на страницы с учетом выбранной категории Глава 9. SportsStore: навигация 267 Модульное тестирование: счетчик товаров определенной категории Протестировать возможность генерации корректных счетчиков товаров для различных категорий очень просто. Мы создадим имитированное хранилище, которое содержит из­ вестные данные в определенном диапазоне категорий, и затем вызовем метод действия List (), запрашивая каждую категорию ProductControllerTests: по очереди. Вот модульный тест, добавленный в класс [Fact] puЬlic 11 void Generate Category_Specific_Product Count() { Организация Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] ( "Catl"}, "Pl", Category 1, Name new Product (ProductID "Cat2"}, "Р2", Category new Product {ProductID = 2, Name "Catl"}, "Р3", Category new Product {ProductID = 3, Name "Cat2"}, "Р4 ", Category new Product {ProductID = 4, Name "Cat3"} "Р5", Category new Product {ProductID = 5, Name }) .AsQueryaЫe<Product>() ); ProductController target = new ProductController(mock.Object); target.PageSize = 3; Func<ViewResult, ProductsListViewModel> GetModel = result => result?.ViewData?.Model as ProductsListViewModel; 11 Действие int? int? int? int? 11 resl res2 = res3 = resAll GetModel(target.List("Catl") )?.Paginginfo.Totalitems; GetModel(target.List("Cat2") )?.Paginginfo.Totalitems; GetModel(target.List("Cat3")) ?.Paginginfo.Totalitems; = GetModel(target.List(null) )?.Paginginfo.Totalitems; Утверждение Assert.Equal(2, Assert.Equal(2, Assert.Equal (1, Assert.Equal(5, resl); res2); res3); resAll); Обратите внимание, что в модульном тесте также вызывается метод List () без указания категории, чтобы удостовериться в правильности подсчета общего количества товаров. Построение корзины для покупок Приложение успешно расширяется, но продавать какие-либо товары до тех пор, пока не будет реализована корзина для покупок, не удастся. В настоящем разделе мы создадим корзину для покупок согласно иллюстрации на рис. 9.7. Если вы приобрета­ ли что-нибудь в электронных магазинах, то она должна выглядеть знакомой. 268 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Товары Ввод информации Ваша корзина о доставке Футбольный мяч 1Добавить в корзину 1 1 х Стадион $79,500.00 Угловые флажки 1Добавить в корзину 1 Всего $79,500.00 Стадион Добавить в корзину ...ит.д ". Перейти к оплате Продолжить покупку Рис. 9.7. Базовый поток корзины для покупок Кнопка доб а вления в корзину {Add to cart) будет отображатьс я рядом с каждым товаром в каталоге. Щелчок на ней будет приводить к выводу сводки по товарам. ко­ торые уже были выбраны пользователем , включая их общую стоимость. В этой точк е пользователь может с помощью кнопки продолжения п о купки (Coпtiпue shoppiпg) воз­ вратиться в каталог товаров, а посредством кнопки перехода к оплате {Checkout now) сформировать заказ и завершить сеан с покупки . Определение модели корзины Начнем с добавления в папку Mode ls файла класса по имени Cart. cs с определ е ­ ниями . показанными в листинге Листинг 9.13. Содержимое файла 9. 13. Cart. cs из папки Models u s ing Syst e m. Co llecti o n s.G eneric; us i n g System.Linq; na me s p ac e Spo rt sSto r e . Models { p uЫ i c c l ass Ca rt { privat e Li s t <CartLi ne> lineColl ectio n = new Lis t<Cart Lin e> ( ) ; рuЫ1с virtual voi d Add item(Pr od u ct produ ct , i nt quan tity) Car tLi n e l i n e = l ineCo l l e c t io n . Wh e r e(p => p . Pr oduct . Pr oductI D == p roduc t . Pro d uctID) . Fi rs t OrDe f au l t() ; if (l i n e == null) { line Co llection.Ad d(new CartL i n e { Pr od u c t = pro duc t, Quant i ty = quantity )) ; el se { line .Qua ntity += qua ntity; puЫ ic vi r tual void Re move Li n e ( Product product) = > li neCo l lectio n. RemoveAll (l => l . Product .Produ c t I D == produc t.P r oductI D) ; puЬlic v i r tual dec i ma l Comput e To t al Value () => lineColl ec tion.Sum( e = > e.Pr oduc t.Price * e.Q uantity ) ; p u Ыi c virt u a l voi d Cl ear() = > lineCo l l e cti o n . Cle a r ( ) ; pu Ьl ic vi r t ua l IEn um e raЫe<Ca rt L i n e> Line s = > l ineCo l le c tion ; Глава 9. SportsStore: навигация 269 puЫic class CartLine { int CartLineID { get; set; ) puЬlic Product Product { get; set; ) puЬlic int Quantity { get; set; ) puЫic Класс Cart использует класс CartLine. который определен в том же самом файле и представляет товар. выбранный пользователем, а также приобретаемое количество товара. Мы определили методы для добавления элемента в корзину. удаления элемен­ та из корзины, вычисления общей стоимости элементов в корзине и очистки корзины путем удаления всех элементов. Мы также предоставили свойство, которое позволяет обратиться к содержимому корзины с применением IEnurneraЫe<CartLine>. Все они легко реализуются с помощью кода С# и небольшой доли кода LINQ. Модульное тестирование: проверка корзины Класс Cart относительно прост, но в нем присутствует ряд важных линий поведения, кото­ рые должны корректно работать. Неправильно функционирующая корзина нарушит работу всего приложения SportsStore. Мы разобьем средства на части и протестируем их по отде­ льности. Для размещения тестов в проекте имени SportsStore. Tests создан новый файл по CartTests. cs. Первая линия поведения относится к добавлению элемента в корзину. При первоначаль­ ном добавлении в корзину объекта CartLine. Product должен быть добавлен новый экземпляр Ниже представлен тестовый метод вместе с определением класса модульного тестирования. using System.Linq; using SportsStore.Models; using Xunit; namespace SportsStore.Tests puЬlic class CartTests { [Fact] puЬlic 11 void Can_Add New Lines() Организация - { создание нескольких тестовых товаров 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[] results = target.Lines.ToArray(); 11 Утверждение Assert.Equal(2, results.Length); Assert.Equal (pl, results [О] .Product); Assert. Equal (р2, resul ts [ 1] . Product); 270 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Но если пользователь уже добавлял объект Product в корзину, тогда необходимо увели­ CartLine, а не создавать новый. Вот мо­ чить количество в соответствующем экземпляре дульный тест: [Fact] puЫic void Can_Add Quantity For Existing Lines() { - создание нескольких тестовых товаров Product pl = new Product { ProductID 1, Name "Pl" }; Product р2 = new Product { ProductID = 2, Name = "Р2" }; 11 Организация 11 Организация - создание Cart target = new Cart () ; 11 новой корзины Действие target.Addltem(pl, 1); target.Addltem(p2, 1); target.Additem(pl, 10); CartLine[] results = target.Lines .OrderBy(c => c.Product.ProductID) .ToArray(); 11 Утверждение Assert.Equal(2, results.Length); Assert. Equal ( 11, resul ts [О] . Quanti ty); Assert. Equal ( 1, resul ts [ 1] . Quanti ty); Также понадобится проверить, что пользователи имеют возможность менять свое решение и удалять товары из корзины. Эта линия поведения реализуется методом RemoveLine () . Модульный тест выглядит следующим образом: [Fact] puЫic void Can_Remove Line() { - создание нескольких Product pl new Product ProductID Product р2 = new Product ProductID Product р3 = new Product ProductID 11 Организация 11 Организация - создание Cart target = new Cart(); товаров 1 , Name "Р 1 " 2, Name "Р2 3, Name "Р3" 11 ); } ; }; новой корзины 11 Организация - добавление target.Additem(pl, 1); target.Additem(p2, 3); target.Additem(p3, 5); target.Addltem(p2, 1); 11 тестовых некоторых товаров в корзину Действие target.RemoveLine(p2); 11 Утверждение Assert.Equal(O, target.Lines.Where(c => c.Product Assert.Equal(2, target.Lines.Count()); р2) .Count ()); Далее проверяется линия поведения, связанная с возможностью вычисления общей стои­ мости элементов в корзине. Вот модульный тест: Глава 9. SportsStore: навигация 271 [Fact] void Calculate Cart Total() { - создание нескольких тестовых товаров "Pl", Price 1, Name ProductID Product pl = new Product Product р2 = new Product { ProductID = 2, Name = "Р2", Price puЫic 11 Организация // Организация - создание Cart target = new Cart(); 11 lOOM ); ); 50М новой корзины Действие target.Additem(pl, 1); target.Additem(p2, 1); target.Additem(pl, 3); decimal result = target.ComputeTotalValue (); 11 Утверждение Assert.Equal(450M, result); Последняя проверка очень проста. Мы должны удостовериться, что в результате очистки корзины ее содержимое корректно удаляется. Ниже показан модульный тест. [Fact] void Can_Clear Contents() { - создание нескольких тестовых 1, Name ProductID Product pl = new Product Product р2 = new Product { ProductID = 2, Name puЬlic 11 11 Организация - создание new Cart(); Организация Cart target = = "Pl", Price "Р2", Price lOOM ); 50М ) ; новой корзины 11 Организация - добавление target.Additem(pl, 1); target.Additem(p2, 1); 11 Действие - очистка target.Clear(); товаров нескольких элементов в корзину корзины //Утверждение Assert.Equal(O, target.Lines.Count()); Временами, как в данном случае, код для тестирования функциональности класса получает­ ся намного длиннее и сложнее, чем код самого класса. Не допускайте, чтобы это приводило к отказу от написания модульных тестов. Дефекты в простых классах, особенно в тех, кото­ рые играют настолько важную роль, как Cart в приложении SportsStore, могут оказывать разрушительное воздействие. Создание кнопок добавления в корзину Нам необходимо модифицировать частичное представление Views/Shared/ ProductSummary. cshtml, добавив кнопки к спискам товаров. В качестве подготов­ ки добавим в папку Infrastructure файл класса по имени UrlExtensions. cs с определением расширяющего метода. приведенного в листинге 9.14. 272 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 9.14. Содержимое файла UrlExtensions. cs из папки Infrastructure using Microsoft.AspNetCore.Http; namespace SportsStore.Infrastructure puЫic static class UrlExtensions { puЫic static string PathAndQuery(this HttpRequest request) => request.QueryString.HasValue ? $"{request.Path){request.QueryString)" : request.Path.ToString(); Расширяющий метод зуемый в URL, работает с классом PathAndQuery () ASP.NET Core для HttpRequest, исполь­ описания НТГР-запроса. Расширяющий метод генерирует по которому браузер будет возвращаться после обновления корзины, принимая во внимание строку запроса, если она есть. В листинге 9.15 в файл импортирования представлений добавляется пространство имен, которое содержит расширяющий ме­ тод, так что его можно применять в частичном представлении. Листинг 9.15. Добавление пространства имен в файле_Viewimports. из папки cshtml Views @using SportsStore.Models @using SportsStore.Models.ViewModels @using SportsStore.Infrastructure @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper SportsStore.Infrastructure.*, SportsStore В листинге 9.16 показано частичное представление с описанием каждого товара, которое обновлено для включения кнопки Add Листинг Productswamary. cshtml 9.16. Добавление кнопки в файле из папки То Cart (Добавить в корзину). Views/Shared @model Product <div class="card card-outline-primary m-l р-1"> <div class="bg-faded р-1"> <h4> @Model.Name <span class="badge badge-pill badge-primary" style="float:right"> <small>@Model.Price.ToString("c")</small> </span> </h4> </div> id="@Model.ProductID" asp-action="AddToCart" asp-controller="Cart" method="post"> <input type="hidden" asp-for="ProductID" /> <input type="hidden" name="returnUrl" value="@ViewContext.HttpContext.Request.PathAndQu ery()" /> <span class="card-text р-1"> <forш @Мodel.Description <button type="suЬmit" class="Ьtn Ьtn-success Ьtn-sm pull-right" style="float:right"> Глава 9. SportsStore: навигация 273 Add То Cart </button> </span> </form> </div> forrn, содержащий скрытые элементы input, которые ус­ ProductID из модели представления и URL, куда браузер после обновления корзины. Элемент forrn и один из элементов Мы добавили элемент танавливают значение должен возвращаться inpu t конфигурируются с использованием встроенных вспомогательных функций де­ скрипторов, что является удобным способом генерирования форм, которые содержат значения модели и нацелены на контроллеры и действия в приложении, как описано в главе 24. Во втором элементе URL для установки input применяется расширяющий метод, созданный возврата. Кроме того, добавлен элемент button, который будет отправлять форму приложению. На заметку! Обратите внимание, что атрибут rnethod элемента forrn установлен в post, что инструктирует браузер относительно отправки данных формы с использованием НТТР-метода POST. Вы можете это изменить и заставить форму применять НТТР-метод GET, но должны соблюдать осторожность. Спецификация НТТР требует, чтобы запросы GET были идемпотентными, т.е. они не должны приводить к изменениям, а добавление товара в корзину определенно считается изменением. Более подробно такая тема будет обсуждаться в главе 16, включая объяснение того, что может произойти, если проигнори­ ровать требование идемпотентности запросов GET. Включение поддержки сеансов Мы собираемся сохранять детали корзины пользователя с использованием состоя­ ния сеанса, что представляет собой данные, которые хранятся на сервере и ассоции­ руются с последовательностью запросов, сделанных пользователем. Инфраструктура ASP.NET предлагает целый ряд разных способов хранения состояния сеанса, в том числе хранение его в памяти, что мы и будем применять. Преимуществом такого под­ хода является простота, но данные сеанса будут утеряны, когда приложение останав­ ливается или перезапускается. Включение поддержки сеансов требует добавления в класс Startup Листинг 9.17. служб и промежуточного программного обеспечения (листинг Включение поддержки сеансов в файле из папки using using using using using using using using using using using Startup. cs SportsStore System; Systern.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; SportsStore.Models; Microsoft.Extensions.Configuration; Microsoft.EntityFrameworkCore; 9.17). 274 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 namespace SportsStore { class Startup { Startup(IConfiguration configuration) => Configuration = configuration; puЫic puЬlic IConfiguration Configuration { get; } void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(optio ns => options.UseSqlServer( Configuration["Data:SportStoreProducts:Connectio nString"])); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddMvc{); puЫic puЬlic services.AddМemoryCache(); services.AddSession(); void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseSession(); app.UseMvc(routes => { puЬlic 11 ... для IHostingEnvironment env) { арр, краткости конфигурация маршрутизации не показана ... ) ) ; SeedData.EnsurePopulated(app); AddMemoryCache ( ) настраивает хранилище данных в памяти. AddSession () регистрирует службы, используемые для доступа к данным се­ а метод UseSession () позволяет системе сеансов автоматически ассоцииро­ Вызов метода Метод анса, вать запросы с сеансами, когда они поступают от клиента. Реализация контроллера для корзины Для обработки щелчков на кнопках Добавим в папку Controllers лением, представленным в листинге Листинг using using using using using 9.18. Add То Cart понадобится создать контроллер. файл класса по имени Содержимое файла CartController. cs CartController. cs из папки System.Linq; Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Mvc; SportsStore.Infrastructure; SportsStore.Models; namespace SportsStore.Controllers class CartController : Controller private IProductRepository repository; puЬlic CartController(IProductRepository repo) { repository = repo; puЬlic с опреде­ 9.18. Controllers Глава 9. SportsStore: навигация 275 RedirectToActionResult AddToCart(int productld, string returnUrl) { Product product = repository.Products . FirstOrDefault (р => р. ProductID == productld); i f (product ! = null) { Cart cart = GetCart(); cart.Addltem(product, 1); SaveCart(cart); puЫic return RedirectToAction("Index", new { returnUrl )) ; RedirectToActionResult RemoveFromCart(int productld, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productld); puЫic i f (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(); return cart; private void SaveCart(Cart cart) { HttpContext.Session.SetJson("Cart", cart); CartController необходимо сделать несколько Cart применяется средство состо­ взаимодействует метод GetCart () . Промежуточное Относительно масса контроллера замечаний. Для сохранения и извлечения объектов яния сеанса ASP.NET, с которым и программное обеспечение, зарегистрированное в предыдущем разделе, использу­ ет сооkiе-наборы или переписывание URL, чтобы ассоциировать вместе множество запросов от определенного пользователя с целью формирования отдельного сеанса просмотра. Связанным средством является состояние сеанса, которое ассоциирует данные с сеансом. Ситуация идеально подходит для масса Cart: мы хотим, чтобы каждый пользователь имел собственную корзину, и она сохранялась между запроса­ ми. Данные, связанные с сеансом, удаляются по истечении времени существования сеанса (обычно из-за того, что пользователь не отправляет запрос какое-то время), т.е. Cart не придется. RemoveFromCart () применялись имена пара­ метров, которые соответствуют именам элементов input в НТМL-формах, созданных в представлении ProductSummary. cshtml. Это позволяет инфраструктуре MVC ассо­ циировать входящие переменные НТГР-запроса POST формы с параметрами и означа­ управлять хранилищем или жизненным цимом объектов В методах действий AddToCart () и ет, что делать что-то самостоятельно для обработки формы не нужно. Такой процесс называется привязкой модели и с его помощью можно значительно упрощать классы контроллеров, как будет объясняться в главе 26. 276 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Определение расширяющих методов состояния сеанса Средство состояния сеанса в и ASP.NET Core хранит только значения Поскольку мы хотим сохранять объект byte []. ширяющие методы для интерфейса ISession, Cart, int, st.ring необходимо определить рас­ которые предоставят доступ к дан­ ным состояния сеанса с целью сериализации объектов ратного преобразования. Добавим в папку SessionExtensions. cs листинге Листинг Cart в формат JSON и их об­ Infrastructure файл класса по имени с определениями расширяющих методов, показанными в 9.19. 9.19. Содержимое файла из папки SessionExtensions. св Infrastructure using Microsoft.AspNetCore.Http; using Newtonsoft.Json; namespace SportsStore.Infrastructure puЫic static class SessionExtensions puЫic static void SetJson(this ISession session, string key, obj ect val ue) { session.SetString(key, JsonConvert.SerializeObject(value)); static Т GetJson<T> (this ISession session, string key) { var sessionData = session.GetString(key); return sessionData == null ? default (Т) : JsonConvert. DeserializeObject<T> (sessionData); puЫic При сериализации объектов в формат ма обозначений для объектов Json.NET, JavaScript) JSON (JavaScript Object Notation - систе­ расширяющие методы полагаются на пакет который вы снова встретите в главе 20. Пакет Json.NET не требуется до­ бавлять к проекту, потому что он уже используется "за кулисами" инфраструктурой МVС для поддержки средства заголовков су JSON. https: / /www.newtonsoft.com/j son с пакетом которое описано в главе 21. (По адре­ доступна информация о том. как работать Json.NET напрямую.) Расширяющие методы облегчают сохранение и извлечение объектов бавления объекта Cart Cart. Для до­ к состоянию сеанса в контроллере применяется следующий вызов: HttpContext.Session.SetJson("Cart", cart); Свойство HttpContext определено в базовом классе обычно унаследованы контроллеры, и возвращает объект Controller, HttpContext. от которого Этот объект предоставляет данные контекста о запросе, который был получен. и ответе, находя­ щемся в процессе подготовки. Свойство реализующий интерфейс где мы определили метод HttpContext. Session возвращает объект. ISession. Данный интерфейс является именно тем типом, SetJson (),принимающий аргументы. в которых указыва­ ются ключ и объект, подлежащий добавлению в состояние сеанса. Глава 9. SportsStore: навигация 277 Расширяющий метод сериализирует объект и добавляет его в состояние сеанса. используя функциональность, которая лежит в основе интерфейса Для извлечения объекта Cart ISession. применяется другой расширяющий метод, которому передается тот же самый ключ:ы Cart cart = HttpContext.Session.GetJson<Cart>("Cart"); Параметр типа позволяет указать тип объекта, который ожидается извлечь: этот тип используется в процессе десериализации. Отображение содержимого корзины Финальное замечание о контроллере и RemoveFromCart () вызывают метод Cart касается того, что методы AddToCart () RedirectToAction (). Результатом будет от­ правка клиентскому браузеру НТГР-инструкции перенаправления, которая заставит URL. В данном случае браузер запросит URL, который вы­ Index () контроллера Cart. Мы планируем реализовать метод Index () и применять его для отображения со­ держимого объекта Cart. Взглянув на рис. 9.7 еще раз, можно заметить, что это рабо­ браузер запросить новый зывает метод действия чий поток, инициируемый по щелчку пользователя на кнопке добавления в корзину. Представлению, которое будет отображать содержимое корзины. необходимо пе­ Cart и URL для отображения в случае, если Continue shopping (Продолжить покупку). Для такой цели мы создадим простой класс модели представления. Добавим в папку Models/ ViewModels проекта SportsStore файл класса по имени CartindexViewModel. cs с содержимым, приведенным в листинге 9.20. редать две порции информации: объект пользователь щелкнет на кнопке Листинг 9.20. Содержимое файла CartindexViewМodel. cs из папки Models/ViewМodels using SportsStore.Models; namespace SportsStore.Models.ViewModels class CartindexViewModel Cart Cart { get; set; } puЫic string ReturnUrl { get; set; puЬlic puЬlic Имея модель представления. можно реализовать метод действия CartController Листинг 9.21. (листинг Реализация метода действия в файле using using using using using using 9.21). CartController. cs System.Linq; Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Mvc; SportsStore.Infrastructure; SportsStore.Models; SportsStore.Models.ViewМodels; Index () из папки Controllers Index () в классе 278 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 namespace SportsStore.Controllers { class CartController : Controller private IProductRepository repository; puЫic CartController(IProductRepository repo) { repository = repo; puЬlic ViewResult Index(string returnUrl) return View(new CartindexViewModel { Cart = GetCart () , ReturnUrl = returnUrl puЬlic }) ; 11 ... для краткости другие методы не показаны ... Метод действия Index () извлекает объект Cart из состояния сеанса и использует CartindexViewModel, который затем передается методу его при создании объекта View () для применения в качестве модели представления. Последний шаг, относящийся к отображению содержимого корзины, предусмат­ ривает создание представления, которое будет визуализировать действие Создадим папку Index. csh tml Листинг 9.22. Views/Cart Index. и поместим в нее файл представления Razoг по имени с разметкой, приведенной в листинге Содержимое файла Index. cshtml 9.22. из папки Views/Cart @model CartindexViewModel <h2>Your cart</h2> <tаЫе class="taЫe taЫe-bordered taЫe-striped"> <thead> <tr> <th>Quantity</th> <th>Item</th> <th class="text-right">Price</th> <th class="text-right">Subtotal</th> </tr> </thead> <tbody> @foreach (var line in Model.Cart.Lines) { <tr> <td class="text-center">@line.Quantity</td> <td class="text-left">@line.Product.Name</td> <td class="text-right">@line.Product.Price.ToString("c")</td> <td class="text-right"> @( {line.Quantity * line.Product.Price) .ToString("c")) </td> </tr> </tbody> Глава 9. SportsStore : навигация 279 <tfoot> <tr> <td colspan="З" class=" text -right">Total:< / td> <td class = "text-right"> @Model. Cart. ComputeTotal Val ue () . ToString ( "с") </td> </ tr> </tfoot> </tа Ые > <div class="text-center"> <а class="btn Ьtn-primary" href="@Model.ReturnUr l " >Continue shopping< /a> < / div> Представление проходит по элементам в корзине и добавляет в НТМL-таблицу строку для каждого элемента вместе со стоимостью и итоговой суммой по корзине . Классы , назначенные элементам, соответствуют стилям Bootstrap для таблиц и вы­ равнивания текста. В результате становится доступной базовая функциональность корзины для поку­ пок. Во-первых, товары выводятся вместе с кнопками ну). как показано на рис. То Cart (Добавить в корзи­ с;1;.1.• Home Thinking Chess lmprove brain efficiency Ьу 75% Soccer Unsteady Chair Watersports Add 9.8. Сар Secretly give your opponent Рис. 9.8. Кнопки Add Во-вторых, щелчок пользователя на кнопке То Add а iфjfiMI disadvantage Cart То Cart приводит к добавлению со­ ответствующего товара в его корзину и отображению сводной информации по корзи­ не (рис. 9.9). Щелчок на кнопке Continue shopping (Продолжить покупку) возвращает пользователя на страницу товара. из которой произошел переход . 280 Часть 1. Введение в инфраструктуру ASP. NET Саге MVC 2 Your cart Home Chess Quantity Soccer Watersports ltem Price Subtotal Unsteady Chair 529.95 $29.95 Thi11king Сар $16.00 $16.00 Human Chess Board 575.00 $75.00 $275.00 $275.00 Total: $395.95 Kayak Continue shopping Рис. 9.9. Отображение содержимого корзины для покупок Резюме В главе мы начали расширять пользовательские части приложения SportsStore. Мы предоставили средства. с помощью которых пользователь может переходить по кате­ гориям, и создали базовые строительные блоки, позволяющие добавлять элементы в корзину для покупок. В следующей главе разработка приложения будет продолжена. ГЛАВА 10 SportsStore: завершение построения корзины для покупок в настоящей главе мы продолжим строить пример приложения SportsStoгe. В пре­ дьщущей главе мы добавили базовую поддержку корзины для покупок, а теперь собираемся усовершенствовать и завершить создание этой функциональности. Усовершенствование модели корзины с помощью службы В предыдущей главе был определен класс модели Cart и продемонстрирована воз­ можность его сохранения с использованием средства состояния сеанса, что давало возможность пользователю формировать набор товаров для покупки. Обязанность по управлению постоянством класса Cart Cart. Cart. возлагалась на контроллер явно определялись методы для получения и сохранения объектов в котором Проблема такого подхода в том. что нам пришлось дублировать код для получения и сохранения объектов Cart во всех компонентах. которые его применяли. В теку­ щем разделе мы воспользуемся средством служб, которое находится в самой основе инфраструктуры ASP.NET Саге, чтобы упростить способ управления объектами освобождая индивидуальные компоненты. такие как контроллер Cart. Cart, от необходи­ мости напрямую иметь дело с деталями. Службы чаще всего применяются для сокрытия деталей реализации интерфейсов от компонентов, которые от них зависят. Вы видели пример, когда создавалась служ­ ба для интерфейса IProductRepository. которая позволила гладко заменить фик­ тивный класс хранилища реальным хранилищем Entity Fгamewoгk Саге. Но службы могут использоваться для решения множества других задач. а также применяться для придания и изменения формы приложения, даже если работа производится с конк­ ретными классами наподобие Cart. Создание класса корзины, осведомленного о хранилище Первым шагом по приведению в порядок способа использования класса Cart будет создание подкласса, который осведомлен о том. как сохранять самого себя с применением состояния сеанса. Добавим в папку SessioпCart. cs Mode 1 s файл класса по имени и поместим в него определение. показанное в листинге 10.1. 282 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Листинг using using using using using 10.1. Содержимое файла SessionCart. cs из папки Models System; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Newtonsoft.Json; SportsStore.Infrastructure; namespace SportsStore.Models { puЫic class SessionCart: Cart { puЫic static Cart GetCart ( IServiceProvider services) { ISession session = services.GetRequiredService<IHttpContextAccessor>()? .HttpContext.Session; SessionCart cart = session?.GetJson<SessionCart>("Cart") ?? new SessionCart(); cart.Session = session; return cart; [Jsonignore] ISession Session { get; set; ) puЫic override void Additem(Product product, int quantity) { base.Additem(product, quantity); Session.SetJson("Cart", this); puЫic override void RemoveLine(Product product) { base.RemoveLine(product); Session.SetJson("Cart", this); puЬlic puЫic override void Clear () base.Clear(); Session.Remove("Cart"); Класс тоды SessionCart является Additem (), RemoveLine () производным от класса и Clear Cart и переопределяет ме­ (),так что они вызывают базовые реали­ зации и затем сохраняют обновленное состояние в сеансе. используя расширяющие методы интерфейса тод GetCart () - ISession, которые были определены в главе это фабрика для создания объектов объектом реализации ISession, 9. SessionCart Статический ме­ и снабжения их чтобы они могли себя сохранять. Получение объекта реализации лучить экземпляр службы к объекту HttpContext. а ISession несколько затруднено. Мы должны по­ IHttpContextAccessor. который предоставит доступ тот в свою очередь - к объекту реализации ISession. Такой непрямой подход требуется из-за того, что сеанс не предоставляется как обыч­ ная служба. Регистрация службы Следующий шаг заключается в создании службы для класса чтобы удовлетворять запросы для объектов Cart Cart. Цель в том. SessionCart. выдачей объектов которые будут сохранять себя самостоятельно. Создание службы иллюстрируется в листинге 10.2. Глава 1О. SportsStore: завершение построения корзины для покупок Листинг 10.2. 283 Создание службы корзины в файле Startup. cs из папки SportsStore puЬlic void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreProducts:ConnectionString"])); services.AddTransient<IProductRepository, EFProductRepository>(); services.AddScoped<Cart>(sp => SessionCart.GetCart(sp)); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddMvc(); services.AddMemoryCache(); services.AddSession(); Метод AddScoped () указывает, что для удовлетворения связанных запросов к Cart должен применяться один и тот же объект. Способ связывания земплярам эк­ за­ просов может быть сконфигурирован, но по умолчанию это значит, что в ответ на любой запрос экземпляра со стороны компонентов, которые обрабатывают тот Cart же самый НТГР-запрос. будет выдаваться один и тот же объект. Вместо предоставления методу AddScoped ( ) отображения между типами, как делалось для хранилища, указывается лямбда-выражение, которое будет выполнять­ ся для удовлетворения запросов к Cart. Лямбда-выражение получает коллекцию служб. которые были зарегистрированы. и передает ее методу SessionCart. В результате запросы для службы создания объектов SessionCart, Cart GetCart () класса будут обрабатываться путем которые сериализируют сами себя как данные се­ анса, когда они модифицируются. Мы также добавили службу с использованием метода AddSingleton (). который указывает, что всегда должен применяться один и тот же объект. Созданная служба сообщает инфраструктуре MVC IHttpContextAccessor, необходимо использовать класс о том, что когда требуются реализации интерфейса HttpContextAccessor. SessionCart можно получать доступ 10.1. Данная служба обязательна. поэтому в классе к текущему сеансу. как делалось в листинге Упрощение контроллера Cart Преимущество создания службы такого вида связано с тем. что она позволит уп­ ростить контроллеры. в которых применяются объекты ден переделанный класс CartController, Cart. В листинге Листинг 10.З. Использование службы Cart в файле CartController. cs из папки using using using using Controllers System.Linq; Microsoft.AspNetCore.Mvc; SportsStore.Models; SportsStore.Models.ViewModels; namespace SportsStore.Controllers { class CartController : Controller private IProductRepository repository; private Cart cart; puЫic 10.3 где задействована новая служба. приве­ 284 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 puЫic CartController(IProductRepository repo, Cart cartService) { repository = repo; cart = cartService; puЫic ViewResult Index(string returnUrl) return View(new CartindexViewModel Cart = cart, ReturnUrl = returnUrl ) ) ; puЫic RedirectToActionResult AddToCart(int productid, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productld); if (product != null) { cart.Additem(product, 1); return RedirectToAction("Index", new { returnUrl )) ; puЬlic RedirectToActionResult RemoveFromCart(int productid, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productid); if (product != null) { cart.RemoveLine(product); return RedirectToAction("Index", new { returnUrl }) ; Класс CartController указывает на то. что он нуждается в объекте Cart. за счет объявления аргумента конструктора. Это позволяет удалить методы. которые читают и записывают данные в сеанс, а также код. требующийся для записи обновлений. Результатом оказывается контроллер. который не только проще. но и более сосредо­ точен на своей роли в приложении, не беспокоясь о том, как объекты Cart создаются либо хранятся. И поскольку службы доступны по всему приложению, любой компо­ нент может получать корзину пользователя с применением одного и того же приема. Завершение функциональности корзины После ввода службы Cart наступило время завершить построение функциональ­ ности корзины. добавив два новых средства. Первое средство позволит пользователю удалять элемент из корзины. Второе средство даст возможность отображать итоговую информацию по корзине в верхней части страницы. Удаление элементов из корзины Мы уже определили и протестировали метод действия троллере. RemoveFromCart () в кон­ а потому для предоставления пользователям возможности удаления эле­ ментов достаточно лишь открыть доступ к данному методу в представлении. доба- Глава 1О. SportsStore: завершение построения корзины для покупок вив кнопки Remove (Удалить) ко всем строкам в итоговой информации по корзине. Изменения, которые понадобится внести в файл заны в листинге Листинг 285 Views/Cart/Index.cshtml, пока­ 10.4. 10.4. Добавление кнопок Remove в файле Index.cshtml из папки Views/Cart @model CartindexViewModel <h2>Your cart</h2> <tаЫе class="taЫe taЫe-bordered taЫe-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.Quantity</td> <td class="text-left">@line.Product.Name</td> <td class="text-right">@line.Product.Price.ToString(" c")</td> <td class="text-right"> @( (line.Quantity * line.Product.Price) .ToString("c")) </td> <td> <forш asp-action="ReшoveFromCart" method="post"> <input type="hidden" name="ProductID" value="@line.Product.ProductID" /> <input type="hidden" name="returnUrl" value="@Мodel.ReturnUrl" /> <Ьutton type="suЬmit" class="Ьtn Ьtn-sm Ьtn-danger"> Remove </button> </form> </td> </tr> </tbody> <tfoot> <tr> <td colspan="З" class="text-right">Total:</td> <td class="text-right"> @Model.Cart.ComputeTotalValue() .ToString("c") </td> </tr> </tf oot> </tаЫе> <div class="text-center"> <а class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a> </div> Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 286 Мы добавили к каждой строке таблицы новый столбец, содержащий элемент со скрытыми элементами и URL возврата. input. form которые указывают товар. подлежащий удалению. а также кнопку для отправки формы. Чтобы увидеть кнопки Remove в работе, понадобится запустить приложение и до­ бавить несколько элементов в корзину для покупок . Поскольку корзина уже обладает функциональностью удаления элементов, ее можно протестировать. щелкнув на од­ ной из новых кнопок (рис. Chess Quantlty Soccer Watersports 10.1). Subtotal Lifejacket $48.95 $48.95 1111 Soccer Ball S19.50 S19.50 Total: $68.45 " ;gщщч.щ;м 1 ~rt Price ltem l ltem Price Subto tal Total: S48.95 НфМ.!.Щфi '-------~ Рис. 10.1. Удаление элемента из корзины для покупок Добавление виджета с итоговой информацией по корзине Мы имеем функционирующую корзину, но еще должны определить способ ее встраивания в пользовательский интерфейс. Пользователи могут выяснять. что на­ ходится в их корзинах, только за счет просмотра экрана со сводкой по корзине. Но попасть на этот экран можно только путем добавления нового элемента в корзину. Для решения упомянутой задачи мы создадим виджет (графический элемент) с итоговой информацией по содержимому корзины. щелчок на котором будет приво­ дить к отображению товаров. находящихся в корзине . и сделаем е го доступным в любом месте приложения. Он будет реализован почти так же, как виджет для нави­ гации - в виде компонента представления, вывод которого может включаться в раз­ деляемую компоновку Razoг. Добавление пакета Font Awesome В качестве части итоговой информации по корзине мы будем отображать кнопку. которая позволит пользователю перейти к оплате. Вместо отображения каких -либо слов на кнопке мы хотим использовать символ корзины. При отсутствии художест­ венных навыков можно применить пакет с открытым кодом Font Awesome, предлагаю­ щий великолепный набор значков, которые допускается интегрировать в приложения как шрифты, где каждый символ шрифта представляет собой отдельное изображение. Получить дополнительные сведения о пакете держащиеся в нем значки. можно по адресу Font Awesome. а также просмотреть со­ ht tps : / / fo nt awesome. сот / . Глава 1О. SportsStore: завершение построения корзины для nокуnок Выберем проект SportsStore элементы) в верхней части окна Добавим пакет Font Awesome и щелкнем на кнопке Solution Explorer, в раздел (Показать все Show All ltems чтобы отобразить файл dependencies (листинг 287 bower. j son. 10.5). Листинг 10.5. Добавление пакета Foпt Awesome в файле bower. j son из папки "name": asp.net 11 11 SportsStore , "private": true, "dependencies": { "bootstrap": "4.0.0-alpha.6", "fontawesome": "4. 7. 0" После сохранения файла мента Bower bower. j son загрузит и установит пакет Visual Studio с помощью инстру­ Font Awesome в папку wwwroot/liЬ/ среда fontawesome. Создание класса компонента представления и представления Добавим в папку Components файл класса по имени CartSummaryViewComponent. cs и определим в нем компонент представления, показанный в листинге Листинг 10.6. 10.6. Содержимое файла CartSummaryViewComponen t. cs из папки Componen ts using Microsoft.AspNetCore.Mvc ; using SportsStore.Models; namespace SportsStore.Components class CartSummaryViewComponent : ViewComponent private Cart cart; puЫic puЫic cart CartSummaryViewComponen t(Cart cartService) ( cartService; = IViewComponentResult Invoke() ( return View(cart); puЫic Компонент представления CartSummaryViewComponen t способен задействовать в своих интересах службу, созданную ранее в главе для получения объекта Cart, при­ нимая ее как аргумент конструктора. Результатом оказывается простой компонент представления, который передает объект Cart методу View (),чтобы сгенерировать фрагмент НТМL-разметки для включения в компоновку. Чтобы создать компонов­ Views/Shared/Components/ CartSummary, добавим в нее файл Razoг по имени Defaul t. cshtml и поместим в него разметку, приве­ ку, создадим папку представления денную в листинге 10.7. 288 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Листинг 1О.7. Содержимое файла Defaul t. csh tml из папки Views/Shared/Components/CartSummary @model Cart <div class=""> @if (Model.Lines.Count() > 0) <small class="navbar-text"> <b>Your cart:</b> @Model.Lines.Sum(x => x.Quantity) item(s) @Model.ComputeTotalValue() .ToString("c") </small> <а class="btn btn-sm btn-secondary navbar-btn" asp-controller="Cart" asp-action="Index" sp-route-returnurl="@ViewContext.HttpContext.Reques t.PathAndQuery()"> <i class="fa fa-shopping-cart"></i> </а> </div> Представление отображает кнопку со значком корзины Font Awesome и когда в корзине присутствуют товары. предоставляет снимок. который сообщает количес­ тво элементов и общую сумму. Теперь, имея компонент представления и представ­ ление, можно модифицировать разделяемую компоновку, чтобы итоговая инфор­ мация по корзине включалась в ответы, генерируемые контроллерами приложения (листинг Листинг 10.8). 10.8. Добавление итоговой информации по корзине в файле _Layout. cshtml из папки Views/Shared <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" asp-href-include="/liЬ/bootstrap/dist/**/*.min.css" asp-href-exclude="**/*-reboot*,**/*-grid*" /> <link rel="stylesheet" asp-href-include="/lib/fontawesome/css/*.css" /> <title>SportsStore</title> </head> <body> <div class="navbar navbar-inverse bg-inverse" role="navigation"> <div class="row"> <а class="col navbar-brand" href="#">SPORTS STORE</a> <div class="col-4 text-right"> @awai t Component. InvokeAsync ( "CartSuшmary") </div> </div> </div> <div class="row m-1 р-1"> <div id="categories" class="col-3"> @awai t Component. InvokeAsync ( "NavigationMenu") </div> Глава 1О. SportsStore: завершение построения корзины для покупок 289 <div class="col -9" > @RenderBody () </div> </div> </ body> </ html> Запустив приложение, можно увидеть итоговую информацию по корзине. Когда корзина пуста, отображается только кнопка Continue shopping (Продолжить покупки). По мере добавления элементов в корзину выводится их количество и общая стоимость (рис. 10.2). Благодаря такому дополнению пользователь знает, какие товары находят­ ся в корзине, и получает очевидный путь для перехода к оплате покупок. Рис. 10.2. Отображение итоговой информации по корзине Отправка заказов Итак, мы добрались до финального пользовательского средства приложения SportsStore: возможности перехода к оплате и оформлению заказа. В последующих разделах мы расширим модель предметной области. чтобы обеспечить поддержку по­ лучения от пользователя подробной информации о доставке. и добавим в приложение обработку этой информации . Создание класса модели Добавим в папку Mo dels Order . cs и приведем его содер ­ Order будет использоваться для пред­ файл класса по имени жимое в соответствие с листингом 10.9. Класс ставления информации о доставке пользователю . Листинг 10.9. Содержимое файла Order. cs из папки Models using System.Collections.Generi c; using System.Compo nentModel.Dat aAnn otations; using Microsoft.AspNetCore.Mvc .ModelBinding; namespace SportsStore.Models puЬlic c la ss Order { 290 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 [BindNever] int OrderID { get; set; ) [BindNever] puЬlic ICollection<CartLine> Lines { get; set; ) [Required(ErrorMessage = "Please enter а name")] puЫic //Введите имя string Name { get; set; ) [Required(ErrorMessage = "Please enter the first address line")] puЫic //Введите первую строку адреса puЫic string Linel get; set; ) puЬlic string Line2 get; set; ) puЬlic string LineЗ get; set; ) [Required(ErrorMessage = "Please enter а city name")] //Введите название города puЬlic string City { get; set; ) [Required(ErrorMessage = "Please enter а state name")] //Введите название штата puЫic string State { get; set; ) string Zip { get; set; ) [Required(ErrorMessage = "Please enter а country name")] // Введите название страны puЫic string Country { get; set; ) puЫic bool GiftWrap { get; set; ) puЫic Здесь применяются атрибуты проверки достоверности из пространства имен System. ComponentModel. DataAnnotations, стоверности подробно рассматривается в главе Кроме того, в классе Order как мы делали в главе 2. Проверка до­ 27. используется атрибут BindNever, который предо­ твращает предоставление пользователем значений для снабженных этим атрибутом свойств в НТТР-запросе. Такая возможность системы привязки моделей, описанная в главе 26, останавливает применение инфраструктурой MVC значений из НТТР­ запроса для заполнения конфиденциальных или важных свойств модели. Добавление реализации процесса оплаты Наша цель заключается в том, чтобы обеспечить пользователям возможность ввода информации о доставке и отправки заказа. Для начала потребуется добавить к представлению итоговой информации по корзине кнопку лате). Изменения, которые необходимо внести в файл показаны в листинге Листинг 10.10. Checkout (Перейти к оп­ Views/Cart/Index.cshtml, 10.10. Добавление кнопки из папки Checkout в файле Index. cshtml Views/Cart <div class="text-center"> <а class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a> <а class="Ьtn Ьtn-primary" asp-action="Checkout" asp-controller="Order"> Checkout </а> </div> Глава 1О. SportsStore: завершение построения корзины для покупок 291 Изменения обеспечивают генерацию ссылки. стилизованной в виде кнопки, щел­ чок на которой приводит к вызову метода действия создаваемого в следующем разделе. На рис. 10.3 Checkout () контроллера Order, показано, как выглядит кнопка Checkout. Your cart Ноте Chess Quantity Price Subtotal $275.00 $275.00 Total: $275.00 Jtem Soccer Kayak Watersports i@,\j.il 11;1 Рис. 10.3. Кнопка Теперь понадобится определить контроллер файл класса по имени ге OrderContro l l er. cs Checkout Order. Добавим в папку Controllers с определением, приведенным в листин­ 10.11. Листинг 10.11. Содержимое файла OrderController.cs из папки Controllers us ing Mi c rosoft .As pNe t Co r e .Mvc ; using SportsSto re.Models; namespace Sport s St ore.Controlle r s puЫic class Or d erController : Controller puЫic ViewRe s ul t Checkout ( ) => Vi e w(n e w Order ()); Checkout ( ) возвращает стандартное представление и передает новый объ­ ShippingDetails в качестве модели представления. Чтобы создать представле­ ние. создадим папку Vi ew s /О rde r и поместим в нее файл представления Razor по имени Chec ko ut. cshtml с разметкой, показанной в листинге 10.12. Метод ект 292 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 10.12. Содержимое файла Checkout. cshtml из папки Views/Order @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"> <hЗ>Ship to</hЗ> <div class="form-group"> <label>Name:</label><input asp-for="Name" class="form-control" /> </div> <h3>Address</h3> <div class="form-group"> <label>Line l:</label><input asp-for="Linel" class="form-control" /> </div> <div class="form-group"> <label>Line 2:</label><input asp-for="Line2" class="form-control" /> </div> <div class="form-group"> <label>Line 3:</label><input asp-for="LineЗ" class="form-control" /> </div> <div class="form-group"> <label>City:</label><input asp-for="City" class="form-control" /> </div> <div class="form-group"> <label>State:</label><input asp-for="State" class="form-control" /> </div> <div class="form-group"> <label>Zip:</label><input asp-for="Zip" class="form-control" /> </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-for="GiftWrap" /> Gift wrap these items </label> </div> <div class="text-center"> <input class="Ьtn Ьtn-primary" type="submit" value="Complete Order" /> </div> </form> Для каждого свойства в модели мы создали элементы вательского ввода. сформатированные с помощью ментах inpu t label и input для пользо­ Bootstrap. Атрибут asp-for в эле­ обрабатывается встроенной вспомогательной функцией дескриптора. которая генерирует атрибуты type, id, name модели, как объясняется в главе и value на основе указанного свойства 24. Чтобы увидеть результат добавления нового метода действия и представления (рис. 10.4). запустим приложение. щелкнем на кнопке со значком корзины в верхней части страницы и затем на кнопке ние можно также, запросив URL Checkout вида (Оплата). Попасть на данное представле­ /Order /Checkout. Глава 1О. SportsStore: завершение nостроения корзины для nокуnок Home Chess Soccer 293 Check out now Please enter your details, and we'll ship your goods right away! Ship to Name: Waterspor1s Address Line 1: Line 2: Line 3: City: State: Zip: Country: Options i:. Gift wrop these items •111к.mm•. •м•м•.1м•м• Рис. 10.4. Форма для сбора деталей о доставке Реализация обработки заказов Мы будем обрабатывать заказы. записывая их в базу данных . Разумеется, боль ­ шинство сайтов электронной коммерции на этом не останавливаются , но мы не ста­ нем заниматься обработкой кредитных карт или других форм оплаты. Чтобы сосре­ доточиться на инфраструктуре данных. MVC. вполне достаточно простого сохранения в базе 294 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Расширение базы данных Когда основной связующий код. созданный в главе 8, на месте, добавить в базу данных новый вид модели легко. Добавим в класс контекста базы данных новое свойс­ тво, как показано в листинге Листинг 10.13. Добавление свойства в фaйneApplicationDbContext.cs из папки using using using using 10.13. Models Microsoft.EntityFrameworkCore; Microsoft.EntityFrameworkCore.Design; Microsoft.EntityFrameworkCore.Infrastructure; Microsoft.Extensions.Dependencyinjection; namespace SportsStore.Models ( puЬlic class ApplicationDbContext : DbContext ( puЫic ApplicationDbContext(DbContextOptions<ApplicationDbCo ntext> options) : base ( options) ( ) puЬlic DbSet<Product> Products ( get; set; puЫic DbSet<Order> Orders { get; set; ) Такого изменения достаточно для того, чтобы инфраструктура Core создала миграцию базы данных, которая позволит объектам Entity Framework Order сохраняться в базе данных. Для создания миграции откроем окно командной строки или перейдем в папку проекта SportsStore (где находится файл PowerShell, Startup. cs) и введем следующую команду: dotnet ef migrations add Orders Эта команда сообщает Entity Framework Core о необходимости получить новый снимок модели данных приложения, выяснить его отличия от предыдущей версии, хранящейся в базе данных, и сгенерировать новую миграцию под названием Orders. Новая миграция будет автоматически применяться при запуске приложения, пото­ му что в классе SeedData вызывается метод Migrate (). предоставляемый Entity Framework Core. Переустановка базы данных Если в модель приходится часто вносить изменения, то может возникнуть ситуация, когда миг­ рации и схема базы данных перестают быть синхронизированными. Самое простое, что можно сделать - удалить базу данных и начать заново. Однако, естественно, такой подход приемлем только на стадии разработки, потому что все сохраненные данные будут утрачены. Чтобы удалить базу данных, необходимо ввести приведенную ниже команду, находясь в пап­ SportsStore: dotnet ef database drop --force ке проекта После того, как база данных удалена, SportsStore ввод следующей команды в папке проекта приводит к построению базы данных заново и применению созданных ра­ нее миграций: dotnet ef database update В результате база данных переустанавливается, точно отражая модель, что дает возмож­ ность возвратиться к разработке приложения. 295 Глава 1О. SportsStore: завершение построения корзины для покупок Создание хранилища заказов Чтобы предоставить доступ к объектам Order, мы последуем тому же самому шаб­ лону. который использовался для хранилища товаров. Добавим в папку файл по имени листинге Листинг IOrderReposi tory. cs Models новый и определим в нем интерфейс, показанный в 10.14. 10.14. Содержимое файла IOrderReposi tory. cs из папки Models using System.Linq; namespace SportsStore.Models puЫic interface IOrderRepository IQueryaЫe<Order> Orders { get; void SaveOrder(Order order); Для реализации интерфейса хранилища заказов добавим в папку класса по имени тинге EFOrderReposi tory. cs Models файл с определением, представленным в лис­ 10.15. Листинг 10.15. Содержимое файла EFOrderReposi tory. cs из папки Models using Microsoft.EntityFrameworkCore; using System.Linq; namespace SportsStore.Models { class EFOrderRepository : IOrderRepository { private ApplicationDbContext context; puЫic EFOrderRepository(ApplicationDbContext ctx) context = ctx; puЫic Orders => context.Orders .Include(o => o.Lines) .Then!nclude(l => l.Product); puЫic IQueryaЬle<Order> void SaveOrder(Order order) context.AttachRange(order.Lines.Select(l => l.Product)); i f (order.OrderID == 0) { context.Orders.Add(order); puЫic context.SaveChanges(); EFOrderRepos i tory реализует интерфейс IOrderReposi tory с примене­ Entity Framework Соге, позволяя извлекать набор сохраненных объектов Order Класс нием и создавать либо изменять заказы. 296 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Особенности хранилища заказов Реализация хранилища для заказов в листинге 10.15 требует небольшой дополнительной работы. Инфраструктуру Eпtity Framework Core необходимо проинструктировать о загрузке связанных данных, если они охватывают несколько таблиц. В листинге тодов Include () и Theninclude () указано, что когда объект 10.15 с Order помощью ме­ читается из базы данных, то также должна загружаться коллекция, ассоциированная со свойством наряду с объектами Product, Lines, которые связаны с элементами коллекции: IQueryaЫe<Order> Orders => context.Orders .Include(o => o.Lines) .Theninclude(l => l.Product); puЫic Такой подход гарантирует получение всех нужных объектов данных, не выполняя запросы и не собирая данные напрямую. Дополнительный шаг требуется и при сохранении объекта Order в базе данных. Когда дан­ ные корзины пользователя десериализируются из состояния сеанса, пакет новые объекты, не известные инфраструктуре Eпtity ется записать все объекты в базу данных. В случае инфраструктура Eпtity Framework Core JSON создает Framework Core, которая затем пыта­ объектов Product это означает, что попытается записать объекты, которые уже были со­ хранены, что приведет к ошибке. Во избежание проблемы мы уведомляем Eпtity Core Framework о том, что объекты существуют и не должны сохраняться в базе данных до тех пор, пока они не будут модифицированы: context.AttachRange(order.Lines.Select(l => l.Product)); В результате инфраструктура Eпtity лизированные объекты Product, Framework Core В листинге 10.16 хранилище заказов ConfigureServices () класса Startup. Листинг 10.16. Order. регистрируется как служба внутри метода Регистрация службы хранилища заказов в файле Startup. св из папки puЫic не будет пытаться записывать десериа­ которые ассоциированы с объектом SportsStore void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreProducts:ConnectionString"))); 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(); Глава 1О. SportsStore: завершение построения корзины для nокуnок Завершение построения контроллера Для завершения класса 297 Order OrderController понадобится модифицировать конс­ труктор так. чтобы он получал службы, требующиеся ему для обработки заказа, и добавить новый метод действия. который будет обрабатывать НТТР-запрос формы. когда пользователь щелкает на кнопке 10.17. Завершение контроллера в файле из папки POST (Завершить заказ). Оба 10.17. изменения показаны в листинге Листинг Complete order OrderController. cs Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; usinq System.Linq; namespace SportsStore.Controllers puЬlic class OrderController : Controller private IOrderRepository repository; private Cart cart; puЫic OrderController(IOrderRepository repoService, Cart cartService) { repository = repoService; cart = cartService; ViewResult Checkout() => View(new Order() ); [HttpPost] puЫic IActionResult Checkout(Order order) { if (cart.Lines.Count() =О) { ModelState.AddМodelError("", "Sorry, your cart is empty! "); puЫic 11 Корзина пуста! if (ModelState.IsValid) { order.Lines = cart.Lines.ToArray(); repository.SaveOrder(order); return RedirectToAction(nameof(Completed)); else { return View(order); ViewResult Completed() cart. Clear () ; return View () ; puЫic Метод действия зываться для Checkout () запроса POST - в декорирован атрибутом HttpPost, т.е. он будет вы­ этом случае, когда пользователь отправляет форму. Мы снова полагаемся на систему привязки моделей. так что можно получить объект Order. дополнить его данными из объекта Cart и сохранить в хранилище. Инфраструктура МVС контролирует ограничения проверки достоверности, кото­ рые бьmи применены к классу через свойство ModelState Order посредством атрибутов аннотаций данных, и сообщает методу действия о любых проблемах. Чтобы вы­ яснить. есть ли проблемы. мы проверяем свойство ModelState. IsValid. Мы вызы- 298 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 ваем метод ModelState.AddModelError () для регистрации сообщения об ошибке. если в корзине нет элементов. Вскоре мы объясним. как отображать такие сообщения об ошибках. а более подробное описание привязки моделей и проверки достоверности будет представлено в главах 27 и 28. Модульное тестирование: обработка заказа Чтобы выполнить модульное тестирование класса рить поведение версии POST метода Checkout тым, использование привязки моделей MVC OrderController, необходимо прове­ ().Хотя метод выглядит коротким и прос­ означает наличие многих вещей, происходящих "за кулисами'', которые должны быть протестированы. Мы хотим обрабатывать заказ, только если в корзине присутствуют элементы и поль­ зователь предоставил достоверные детали о доставке. При любых других обстоятельс­ твах пользователю должно быть сообщено об ошибке. Вот первый тестовый метод, кото­ рый определен в файле класса по имени OrderControllerTests. cs внутри проекта SportsStore. Tests: using Microsoft.AspNetCore.Mvc; using Moq; using SportsStore.Controllers; using SportsStore.Models; using Xunit; namespace SportsStore.Tests puЬlic class OrderControllerTests [Fact] puЫic void Cannot_Checkout_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.IsAny<Order>()), Times.Never); 11 Утверждение - проверка, что метод возвращает стандартное представление Assert.True(string.IsNullOrEmpty(result.ViewName) ); 11 Утверждение - проверка, что представлению передана 11 недопустимая модель Assert.False(result.ViewData.ModelState.IsValid); Тест проверяет отсутствие возможности перехода к оплате при пустой корзине. Мы удосто­ веряемся, что метод SaveOrder () имитированной реализации IOrderReposi tory ни­ когда не вызывается, что метод возвращает стандартное представление (которое повторно отобразит введенные пользователем данные, давая ему шанс откорректировать их) и что состояние модели, передаваемое представлению, помечено как недопустимое. Это может выглядеть как излишне ограничивающий набор утверждений, но для проверки правильности Глава 1О. SportsStore: завершение построения корзины для покупок 299 поведения нужны все три утверждения. Следующий тестовый метод работает в основном так же, но внедряет в модель представления ошибку, эмулирующую проблему, о которой сообщает средство привязки модели (что должно происходить в производственной среде, когда пользователь вводит некорректные данные о доставке): [Fact] puЫic void Cannot Checkout Invalid_ShippingDetails() { - создание имитированного хранилища заказов Mock<IOrderRepository> mock = new Mock<IOrderRepository>(); // Организация - создание корзины с одним элементом Cart cart = new Cart(); cart.Additem(new Product(), 1); //Организация - создание экземпляра контроллера OrderController target = new OrderController(mock.Object, cart); //Организация - добавление ошибки в модель target.ModelState.AddModelError("error", "error"); // Действие - попытка перехода к оплате ViewResult result = target.Checkout(new Order()) as ViewResult; // Утверждение - проверка, что заказ не был сохранен 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 Submit Order() { - создание имитированного хранилища заказов Mock<IOrderRepository> mock = new Mock<IOrderRepository>(); / / Организация - создание корзины с одним элементом Cart cart = new Cart(); cart.Addltem(new Product(), 1); //Организация - создание экземпляра контроллера OrderCoпtroller 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. Введение в инфраструктуру ASP.NET Core MVC 2 300 Отображение сообщений об ошибках проверки достоверности Для проверки пользовательских данных инфраструктура МVС будет использовать атрибуты проверки достоверности. примененные к классу Order. Тем не менее. чтобы отобразить сообщения о проблемах, понадобится внести небольшое изменение. Здесь задействована еще одна встроенная вспомогательная функция дескриптора . которая инспектирует состояние проверки достоверности данных. предоставленных пользова­ телем. и добавляет предупреждающие сообщения для каждой обнаруженной проблемы. В листинге HTML, 10.18 демонстрируется добавление в файл Ch e ckout . cs h tm l элемента который будет обрабатываться этой вспомогательной функцией дескриптора. Листинг 10.18. Добавление области с итогами проверки достоверности в файле Checkout.cshtml из папки Views/Order @model Or d er <h2>Check out n ow</ h 2> <p>Please enter your details, an d we'll s h ip yo u r goods ri gh t away ! </ p > <div asp-validation-summary="All" class="text-danger"></div> <form asp-actio n="Checkout" metho d="post" > < hЗ>Ship to </ h З> Благодаря такому про стому изм ен е нию пользо вателю отображаются сообщения об ошибках проверки достоверности. Чтобы увидеть резул ьтат, понадобится посетить URL вида и попробовать перейти к оплате, не выбрав ни одного /Order / Ch eckout товара или не указав сведения о доставке (рис. 10.5). Вспомогательная функция де­ скриптора, генерирующая такие сообщения. является частью системы проверки до­ стоверности моделей, которая подробно рассматривается в главе Horne Chess Soccer Watersports 27. Check out now Please enter your details, and l'>'e'll ship your goods right away! • • • • • • Please enter а name Please enter the first address line Please erнer а city riame Please enter а state nan1e Please enter а country name Sorry, your cart is en1pty! Ship to Рис. 1О.5. Отображение сообщений об ошибках проверки достоверности Глава 1О. SportsStore: завершение построения корзины для покупок 301 Совет. Данные, отправленные пользователем, перед проверкой посылаются серверу, что известно как проверка достоверности на стороне сервера , для которой инфраструктура MVC предлагает великолепную подцержку. Проблема с проверкой достоверности на сто­ роне сервера заключается в том , что пользователю сообщается об ошибках лишь после того, как данные отправлены серверу и обработаны, а также сгенерирована результирую­ щая страница - на занятом сервере все это может занять несколько секунд . По указан­ ной причине проверка достоверности на стороне сервера обычно дополняется провер­ кой достоверности на стороне клиента, при которой введенные пользователем значения проверяются с помощью кода JavaScript до отправки серверу данных формы . Проверка достоверности на стороне клиента будет описана в главе 27. Отображение итоговой страницы Чтобы завершить реализацию процесса оплаты , необходимо создать представ­ ление. которое будет отображаться, когда браузер перенаправляется на де йствие Comp l eted контроллера Orde r . Добавим в папку Vi ew s / Order файл представления Razor по имени Compl et ed. c sh t ml и поместим в него разметку. приведенную в лис­ тинге 10.19. Листинг 10.19. Содержимое файла Completed. cshtml из папки Views/Order <h 2> Thanks 1 </h2> <p>T h a n ks for pla c ing your order . </p> <p >We 'll ship you r g oods a s s oon a s p os s i Ы e.< / p> Для интеграции представления Completed в приложение никаких изменений в код вносить не придется, поскольку требуемые операторы уже были добавлены при определении метода действия Comp le t ed () в листинге 10.17. Теперь пользователь может проходить через весь процесс, начин ая с выбора товаров и заканчивая пере­ ходом к оплате. При условии. что пол ьзов атель пр едоставил корр ектные сведения о доставке (и в корзине есть какие-то товары), после щелчка на кнопке он увидит итоговую страницу (рис . 10.6). Thanks! Home Thanks for placing your order. Chess We'JJ sl1ip your goods as soon as possiЬJe. Soccer Watersports Рис. 10.6. Итоговая страница завершенного заказа Complete order 302 Часть 1. Введение в инфраструктуру ASP.NEТ Core MVC 2 Резюме Мы завершили все основные части приложения SportsStore. отвечающие за вза­ имодействие с пользователями. Конечно, до сайта Amazoп приложению далеко, но в нем имеется каталог товаров с возможностью просмотра по категориям и страницам, аккуратная корзина для покупок и простой процесс оплаты. Архитектура с хорошим разделением означает, что мы можем легко изменять поведение любой порции приложения. не беспокоясь о возникновении проблем или несовместимости где-либо в приложении. Например. мы могли бы изменить способ сохранения заказов, что никак бы не повлияло на корзину для покупок. каталог това­ ров или любую другую область приложения. В следующей главе мы добавим средства. необходимые для администрирования приложения SportsStore. ГЛАВА 11 SportsStore: администрирование в настоящей главе мы продолжим строить приложение SportsStore. чтобы предо­ ставить администратору сайта способ управления заказами и товарами. Управление заказами В предыдущей главе была добавлена поддержка для получения заказов от пользо­ вателей и сохранения их в базе данных. В этой главе мы собираемся создать простой инструмент администрирования. который позволит просматривать полученные зака­ зы и помечать их как отгруженные. Расширение модели Первым изменением, которое необходимо внести, является расширение модели, чтобы можно было фиксировать, какие заказы были отгружены. В листинге показано добавление нового свойства в класс Order. cs Листинг внутри папки 11.1. Models. Добавление свойства в файле Order. cs из папки Models using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace SportsStore.Models puЫic 11. l Order, который определен в файле class Order { [BindNever] puЫic int OrderID { get; set; ) [ BindNever] puЫic ICollection<CartLine> Lines { get; set; ) [BindNever] bool Shipped { get; set; } puЬlic [Required(ErrorMessage = "Please enter / / Введите имя puЫic string Name { get; set; ) а name")] [Required(ErrorMessage = "Please enter the first address line")] 11 Введите первую строку адреса 304 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 puЫic puЫic puЫic string Linel string Line2 string LineЗ get; set; get; set; get; set; [Required(ErrorMessage = "Please enter 11 puЫic = "Please enter 11 puЫic = "Please enter 11 puЫic а state narne")] Введите название штата string State { get; set; 1 string Zip { get; set; 1 [Required(ErrorMessage puЫic city narne")J string City { get; set; 1 [Required(ErrorMessage puЫic а Введите название города а country narne")] Введите название страны string Country { get; set; ) bool GiftWrap { get; set; 1 Такой итеративный подход к расширению и адаптации модели с целью поддержки различных средств типичен при разработке приложений MVC. В идеальном мире у нас была бы возможность полностью определить классы моделей в начале проекта и просто строить приложение на их основе, но так случается только в простейших про­ ектах, а на практике следует ожидать итеративную разработку по мере понимания того, что требуется разрабатывать и развивать. Миграции Entity Framework Core облегчают этот процесс, поскольку нам не при·· ходится вручную удерживать схему базы данных в синхронизированном состоянии с классами моделей, создавая и запуская команды бы отразить добавление свойства строки или PowerShell, Shipped SQL. Обновим базу данных, что­ в класс перейдя в папку проекта Order, открыв окно командной SportsStore (ту, что содержит файл и выполнив следующую команду: Startup. cs) dotnet ef rnigrations add ShippedOrders Миграция будет применена автоматически, когда приложение запустится и в клас­ се произойдет обращение к методу SeedData структурой Migrate (),предоставленному инфра­ Entity Framework Core. Добавление действий и представления Функциональность, требуемая для отображения и обновления набора заказов в базе данных, относительно проста, потому что она строится на основе средств и ин­ фраструктуры, которые были созданы в предшествующих главах. В листинге контроллеру Листинг Order 11.2. Добавление двух методов действий в файле из папки Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using Systern.Linq; narnespace SportsStore.Controllers class OrderController : Controller private IOrderRepository repository; puЫic 11.2 добавляются два метода действий. OrderController. cs к Глава 11. SportsStore: администрирование 305 private Cart cart; OrderController(IOrderRepo sitory repoService, Cart cartService) { repository = repoService; cart = cartService; puЫic ViewResul t List () => View(repository.Orders.Wh ere(o => !o.Shipped)); puЫic [HttpPost] IActionResult мarkShipped(int orderID) ( Order order = repository.Orders orderID); .FirstOrDefault(o => o.OrderID if {order != null) ( order.Shipped = true; repository.SaveOrder(orde r); puЬlic == return RedirectToAction(nameof( List)); puЫic ViewResult Checkout() => View(new Order()); (HttpPost] IActionResult Checkout(Order order) { if (cart.Lines.Count() == 0) { ModelState.AddModelError ("", "Sorry, your cart is empty!"); puЫic if (ModelState. IsValid) { order.Lines = cart.Lines.ToArray(); repository.SaveOrder(ord er); return RedirectToAction(nameof(C ompleted)); else { return View(order); ViewResult Completed() { cart.Clear(); return View (); puЬlic Метод List () выбирает все объекты рых имеет значение твия List () false, Order в хранилище. свойство Shipped кото­ и передает их стандартному представлению. Метод дейс­ будет использоваться для отображения администратору списка неотгру­ женных заказов. MarkShipped () будет получать запрос POST, указывающий идентифика­ Order из хранилища, чтобы установить его свойство Shipped в true и сохранить. Для отображения списка неотгруженных заказов добавим в папку Views/Order файл представления Razor по имени List. cshtrnl и поместим в него разметку из Метод тор заказа, который применяется для извлечения соответствующего объекта листинга 11.3. Элемент tаЫе используется для отображения ряда деталей, включая сведения о приобретенных товарах. 306 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 11.3. @model IEnumeraЫe<Order> Содержимое файла List.cshtml из папки Views/Order @{ ViewBag.Title = "Orders"; Layout = "_AdminLayout"; @if (Model.Count() > 0) { <tаЫе class="taЫe taЫe-bordered taЫe-striped"> <tr><th>Name</th><th>Zip</th><th colspan="2">Details</th><th></th></tr> @foreach (Order о in Model) { <tr> <td>@o.Name</td><td>@o.Zip</td><th>Product</th>< th>Quantity</th> <td> <form asp-action="MarkShipped" method="post"> <input type="hidden" name="orderid" value="@o.OrderID" /> <button type="submit" class="btn Ьtn-sm Ьtn-danger"> Ship </button> </form> </td> </tr> @foreach (CartLine line in o.Lines) { <tr> <td colspan="2"></td> <td>@line.Product.Name</td><td>@line.Quantity</td > <td></td> </tr> </tаЫе> else { <div class="text-center">No Unshipped Orders</div> Каждый заказ отображается с кнопкой му методу действия List MarkShipped Ship (Отгрузить), которая отправляет фор­ ().С помощью свойства Layout для представления указана другая компоновка, которая переопределяет компоновку. заданную в файле_ ViewStart. cshtml. Для добавления компоновки создадим в папке AdminLayout. cshtml Views/Shared с применением шаблона элемента файл по имени MVC View Layout Page (Страница компоновки представления МVС) и поместим в него разметку, показанную в листинге Листинг 11.4. 11.4. Содержимое файла _AdminLayout.cshtml из папки Views/Shared <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> <title>@ViewBag.Title</title> </head> Глава 11. SportsStore: администрирование 307 <body c l as s = "m- 1 р -1" > <div c l ass ="bg-info p- 2 " >< h4 >@Vi ewBag.Titl e< / h 4 ></ div> @RenderBody ( ) </body> </ html> Чтобы просматривать и управлять заказами в приложении, запустим приложение , выберем некоторые товары и перейдем к оплате. Затем посетим L i s t. Появится сводка по созданным заказам (рис. 11.1). URL вида /O r de r / Щелкнем на кнопке Ship; база данных обновится. а список ожидающих заказов будет пуст. С) Ord«s Orders Name Zlp Details Alice 10036 Product Quantity Lifejacket Soccer Ball Рис. 11.1. Управление заказами На заметку! В настоящий момент ничто не может воспрепятствовать запросу пользователем URL вида / Order / Li s t и администрированию своего заказа. В главе 12 объясняется, как ограничить доступ к методам действий. Добавление средств управления каталогом Соглаш ение для управления более сложными коллекциями элементов предусмат ­ ривает предоставление пользователю страниц двух типов : страницы списка и страни ­ цы редактирования (рис. 11.2). Edit ltem: Kayak List Screen \tem Actions Kayak Edlt 1Delete Description: \ А boat for one ре". Life)acket Edit 1Oelete Cзtegory: Watersports Soccer Ьа11 ШI .12..е.!Ш Price ($): 275.00 Add New ltem Рис. 11.2. \ Kayak Name: ::::::====~ Save Эскиз пользовательского интерфейса CRUD Cancel для каталога товаров 308 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Вместе эти страницы позволяют пользователю создавать, читать, обновлять и удалять (create, read, update. delete - CRUD) называются onepat~uямu CRUD CRUD. элементы в коллекции. Такие действия Разработчики нуждаются в реализации операций настолько часто, что средство формирования шаблонов в гает сценарии для создания контроллеров CRUD Visual Stud!o предла­ с заранее определенными методами действий (включение средства формирования шаблонов рассматривалось в главе Но, как и со всеми шаблонами NET Core MVC Visual Studio, я считаю, что изучать возможности 8). ASP. лучше напрямую. Создание контроллера CRUD Начнем с создания отдельного контроллера для управления каталогом товаров. Добавим в папку Controllers приведенным в листинге Листинг 11.5. файл класса по имени AdminController. cs с кодом, 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Ыic В ( ViewResult Index() => View(repository.Products); конструкторе контроллера IProductRepository, объявлена зависимость В классе контроллера определен единственный метод действия вызывает метод View от интерфейса которая будет распознаваться при создании экземпляров. Index (), который (),чтобы выбрать стандартное представление для действия, и передает ему в качестве модели представления набор товаров из базы данных. Модульное тестирование: метод действия Нас интересует поведение метода действия Index () Index () в контроллере Admin, которое за­ Product из хранилища. Протестировать ключается в корректном возвращении объектов его можно за счет создания имитированной реализации хранилища и сравнения тестовых данных с данными, которые возвращает метод действия. Ниже показан код модульного теста, помещенный в новый файл по имени SportsStore.Tests. using using using using using using using System.Collections.Generic; System.Linq; Microsoft.AspNetCore.Mvc; Moq; SportsStore.Controllers; SportsStore.Models; Xunit; AdminControllerTests. cs внутри проекта Глава 11. SportsStore: администрирование 309 namespace SportsStore.Tests { class AdminControllerTests [ Fact] puЬlic void Index_Contains_All_Products() //Организация - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[] { "Pl"), 1, Name new Product {ProductID "Р2") , new Product { Product I D = 2, Name "РЗ"), new Product {ProductID = 3, Name )) .AsQueryaЫe<Product>() ); //Организация - создание контроллера AdminController target = new AdminController(mock.Object); puЫic 11 Действие Product[] result = GetViewModel<IEnumeraЫe<Product>>(target.Index() )?.ToArray(); //Утверждение result.Length); Assert.Equal ("Pl", resu.lt[O] .Name); Assert.Equal("P2", result[l].Name); Assert.Equal ("РЗ", result[2] .Name); Assert.Equal(З, private Т GetViewModel<T>(IActionResu.lt resu.lt) where return (result as ViewResult)?.ViewData.Model as Т; В тест был добавлен метод GetViewModel () Т c.lass { для распаковки результата, возвращаемого методом действия, и получения данных модели представления. Далее в главе будут реали­ зованы дополнительные тесты, которые используют этот метод. Реализация представления списка Следующим шагом будет добавление представления для метода действия контроллера ния Razor Admin. по имени Создадим папку Index. cshtml Содержимое файла Листинг 11.6. @mode.l IEnumeraЬle<Product> Views/Admin с содержимым, приведенным в листинге Index. cshtml Index () и добавим в нее файл представле­ из папки Views/Admin @{ ViewBag.Title = "A.ll Products"; Layout = "_AdmiпLayout"; <tab.le c.lass="taЫe taЬle-striped tab.le-bordered tab.le-sm"> <tr> <th c.lass="text-right">ID</th> <th>Name</th> <th c.lass="text-right">Price</th> <th class="text-center">Actions</th> </tr> 11.6. 310 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 @foreach (var item in Model) { <tr> <td class = "text-right">@item.ProductID</td > <td>@item.Name</td > <td class="text-right">@item.Price.ToString("c")</td> < td class= "text-center"> <form asp-action="Delete" method="post"> <а asp-action = "Edit" class = "Ьtn Ьtn-sm Ьtn-warning" asp -route-productid="@item.ProductID"> Edit </а> <input type="h idde n" name = "Produc tID" value= "@item.P roductl D" /> <button type = "submit" clas s= "Ьtn Ьtn -danger Ьtn-sm" > Delete </button> </form > </td> </tr> < /tаЫе> <di v class = "text-center" > <а asp-a ctio n = "Create" class="Ьtn </d iv> Ьtn-primary">Add Produ ct</a> Представление содержит таблицу, в которой для каждого товара предусмотрена строка с ячейками, содержащими наименование и цену товара. Кроме того. в каждой строке присутствуют кнопки. которые позволят редактировать сведения о товаре и удалять его, отправляя запросы к действиям имеется кнопка Add Product добавим действия Edi t и Delete . В дополнение к таблице (Добавить товар). нацеленная на действие Edi t, Dele te и Creat e Create. посмотреть, как отображаются товары. запустив приложение и запросив /Admin/Index (рис. 11.3). All Products 10 NAme Price 1 K•yak S27S.OO Шejaclc:e1 148.95 3 Socct>r Ball S1950 2 б Thinicing Act1ons 134.95 4 Corner Flags S Stadium 579.\0000 Сар \16.00 7 Unsteady Chair 129.95 8 Hum.an Chбs 8oard 9 Bling·Blirig King Мы в последующих разделах, а пока можно 175.00 11.200.00 if!'i* Рис. 11.З. Отображение списка товаров URL вида Глава 11. SportsStore: администрирование Совет. В листинге 11.6 кнопка Edit (Редактировать) находится внутри элемента две кнопки располагаются рядом благодаря интервалу, примененному Edit будет посылать серверу НТТР-эапрос это не требует элемента forrn. forrn, Bootstrap. так что Кнопка GET для получения текущих сведений о товаре; Однако поскольку кнопка Delete (Удалить) будет вносить изменение в состояние приложения, необходимо использовать НТТР-эапрос рый требует элемента 311 POST, кото­ forrn. Редактирование сведений о товарах Чтобы предоставить средства создания и обновления, мы добавим страницу ре­ дактирования сведений о товаре, подобную показанной на рис. 1 1.2. Задача состоит из двух частей: • отобразить страницу, которая позволит администратору изменять значения для свойств товара; • добавить метод действия, который обработает внесенные изменения, когда они будут отправлены. Создание метода действия В листинге Adrnin. 11. 7 Edi t () приведен код метода действия Edi t ( ) , добавленного в контроллер который будет получать НТГР-запрос, отправляемый браузером, когда пользо­ ватель щелкает на кнопке Листинг 11.7. Edit. Добавление метода действия из папки Edi t () в файле AdminController. cs Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; namespace SportsStore.Controllers puЫic class AdminController : Controller private IProductRepository repository; puЫic AdminController(IProductRepository repo) { repository = repo; puЫic ViewResult Index() => View(repository.Products); ViewResul t Edi t (int productid) => View(repository.Products productid)); .FirstOrDefault(p => p.ProductID puЬlic == Этот простой метод ищет товар с идентификатором, соответствующим значе­ нию параметра View (). productld, и передает его как объект модели представления методу 312 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Модульное тестирование: метод действия Edi t О В методе действия Edi t () нам необходимо протестировать две линии поведения. Первая заключается в том, что мы получаем запрашиваемый товар, когда предоставляем допусти­ мое значение идентификатора, чтобы удостовериться в редактировании ожидаемого това­ ра. Вторая проверяемая линия поведения связана с тем, что мы не должны получать товар при запросе значения идентификатора, отсутствующего в хранилище. Ниже показаны тес­ товые методы, добавленные в файл класса AdminControllerTests. cs. [Fact] void Can_Edit Product() { - создание имитированного хранилища 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 "Р3"), )) .AsQueryaЬle<Product>()); 11 Организация - создание контроллера AdminController target = new AdminController(mock.Object); puЫic 11 Организация 11 Действие Product pl = GetViewModel<Product>(target.Edit(l)); Product р2 = GetViewModel<Product>(target.Edit(2)); Product р3 = GetViewModel<Product>(target.Edit(3)); 11 Утверждение Assert.Equal(l, pl.ProductID); Assert.Equal(2, p2.ProductID); Assert.Equal(3, p3.ProductID); [Fact] puЫic 11 void Cannot_Edit_Nonexistent Product() { Организация - создание имитированного хранилища 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 "Р3"), )) .AsQueryaЬle<Product>()); 11 Организация - создание контроллера AdminController target = new AdminController(mock.Object); 11 Действие Product result 11 = GetViewModel<Product>(target.Edit(4)); Утверждение Assert.Null(result); Глава 11. SportsStore: администрирование 313 Создание представления редактирования Теперь, располагая методом действия, можно создать представление для отображе­ ния. Добавим в папку Views/Admin Листинг 11.8. Содержимое файла Razor по имени Edi t. cshtml 11.8. файл представления и поместим в него разметку, приведенную в листинге Edit.cshtml из папки Views/Admin @model Product @{ ViewBag.Title = "Edit Product"; Layout = "_AdmiпLayout"; method="post"> asp-for="ProductID" /> <div class="form-group"> <label asp-for="Name"></label> <iпput asp-for="Name" class="form-coпtrol" /> </div> <div class="form-group"> <label asp-for="Descriptioп"></label> <textarea asp-for="Descriptioп" class="form-coпtrol"></textarea> </div> <div class="form-group"> <label asp-for="Category"></label> <iпput asp-for="Category" class="form-coпtrol" /> </div> <div class="form-group"> <label asp-for="Price"></label> <iпput asp-for="Price" class="form-coпtrol" /> </div> <div class="text-ceпter"> <form asp-actioп="Edit" <iпput type="hiddeп" <buttoп class="btп btп-primary" type="submit">Save</buttoп> <а asp-actioп="Iпdex" class="btп btп-secoпdary">Cancel</a> </div> </form> В представлении имеется форма HTML, большая часть содержимого которой гене­ рируется посредством вспомогательных функций дескрипторов, включая установку целей для элементов рибутов name, id и form value и а, установку содержимого элементов для элементов input и label и выдачу ат­ textarea. Чтобы увидеть НТМL-разметку. генерируемую представлением, запустим прило­ жение, перейдем на товаров (рис. URL типа / Admin/ Index Совет. Скрытый элемент Значение и щелкнем на кнопке Edit для одного из 11.4). ProductID inpu t для свойства Р roduc t I D применяется ради простоты. генерируется базой данных как первичный ключ, когда новый объ­ ект сохраняется инфраструктурой Eпtity Framework Core, и его безопасное изменение может оказаться сложным процессом. Для большинства приложений проще всего предо­ твратить изменение значения со стороны пользователя. 314 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 D Edrt Product С Х ф localhost Edit Product Name 1 Soccer Ball Description FIFA-approved s1ze and we1ght Category Soccer Price 19 50 - Рис. 11.4. Cancel Отображение сведений о товаре для редактирования Обновление хранилища товаров Прежде чем можно будет о брабат ыват ь р езул ьтаты р еда ктирова ния . хр а нили ­ ще товар ов п о надобится рас ширить. добавив возм ожность сохран ения из м ен е ний . Первым делом необходимо добавить к интерфейсу тод (листинг Листинг I Produ ctRe p o s i t o ry новый ме­ 11.9). 11.9. Добавление метода в файл IProductResposi tory. cs из папки Models us i ng Sys tem.Linq; na mespace Spo rt sSto re.Model s p u Ьlic interfa c e I Produc t Re po s ito ry IQu e ryaЫe< P r od uct > Pr od uc ts { ge t; void SaveProduct(Product product); З атем можно добавить новый метод к реализ а ции хранилища с помощью Entity Framework (листинг 11.10). С о ге, которая определена в фай л е EFProd uct Repo s i to r y . c s 315 Глава 11. SportsStore: администрирование Листинг 11.1 О. Реализация нового метода в файле из папки EFProductReposi tory. cs Model s using System.Collections.Generic; using System.Linq; namespace SportsStore.Models { class EFProductRepository : IProductRepository { private ApplicationDbContext context; puЫic EFProductRepository(ApplicationDbContext ctx) { context = ctx; puЬlic IQueryaЫe<Product> Products => context.Products; void SaveProduct (Product product) if (product. ProductID == 0) { 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; puЫic puЫic context.SaveChanges(); Реализация метода ProductID SaveChanges () добавляет товар в хранилище, если значение равно О; в противном случае применяются изменения к существующей записи в базе данных. Мы не хотим здесь вдаваться в детали инфраструктуры Entity Framework Core, пос­ кольку. как упоминалось ранее, это отдельная крупная тема. к тому же она не являет­ ся частью ASP.NEТ Core MVC. Тем не менее, кое-какой код в методе SaveProduct () оказывает влияние на проектное решение. положенное в основу приложения МVС. Нам известно. что обновление должно выполняться. когда получен параметр Product, который имеет ненулевое значение влечения из хранилища объекта Product ProductID. Задача решается путем из­ с тем же самым значением ProductID и обновления всех его свойств, чтобы они соответствовали значениям свойств объекта, переданного в качестве параметра. Причина таких действий в том, что инфраструктура Entity Framework Core отсле­ живает объекты. которые она создает из базы данных. Объект. переданный методу SaveChanges (). создается системой привязки моделей Entity Framework Core ничего не знает о новом объекте менять обновление к базе данных, коrда объект Product MVC, т.е. инфраструктура Product, и она не будет при­ модифицирован. Существует множество способов решения указанной проблемы. но мы принимаем самый простой из них. предполагающий поиск соответствующего объекта, о котором известно инф­ раструктуре Entity Framework Core, и его явное обновление. 316 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Добавление нового метода в интерфейс класса имитированного хранилища в главе 8. IProductRepository нарушает FakeProductReposi tory, который был работу создан Имитированное хранилище использовалось в целях быстрого старта про­ цесса разработки и демонстрации возможности применения служб для гладкой за­ мены реализаций интерфейса, не изменяя компоненты. которые на них опираются. Имитированное хранилище больше не понадобится. В листинге терфейс IProductReposi tory 11. 11 видно, что ин­ удален из объявления класса. поэтому продолжать мо­ дификацию класса по мере добавления функций хранилища не придется. Листинг 11.11. Удаление интерфейса в файле из папки FakeProductReposi tory. cs Models using System.Collections.Generic; using System.Linq; namespace SportsStore.Models { puЫic сlавв FakeProductRepository /* : IProductRepository */ Products => new List<Product> new Product { Name "Football", Price = 25 ), new Product { Name = "Surf board", Price = 179 ) , new Product { Name = "Running shoes", Price = 95 ) puЬlic IQueryaЫe<Product> ).AsQueryaЫe<Product>{); Обработка запросов POST в методе действия Edi t () К настоящему моменту все готово для реализации в контроллере женной версии метода действия Edi t (), инициируемые по щелчку администратором на кнопке метода приведен в листинге Листинг 11.12. Save AdminController. cs Controllers namespace SportsStore.Controllers class AdminController : Controller private IProductRepository repository; puЫic AdminController(IProductRepository repo) { repository = repo; puЫic ViewResult Index() => View(repository.Products); ViewResult Edit(int productld) => View(repository.Products .FirstOrDefault(p => p.ProductID == productld)); puЬlic [HttpPost] POST. (Сохранить). Код нового using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; puЫic перегру­ 11.12. Определение метода действия в файле из папки Admin которая будет обрабатывать запросы Глава 11. SportsStore: администрирование 317 IActionResul t Edi t (Product product) if (ModelState.IsValid) { repository.SaveProduct(product); TempData["message"] = $ 11 {product.Name} has been saved"; return RedirectToAction("Index"); else { puЫic //Что-то не так со значениями данных return View(product); Мы выясняем. смог ли процесс привязки модели проверить достоверность отправленных пользователем ModelState. IsValid. для данных, чего читаем значение свойства Если здесь все в порядке, тогда мы сохраняем изменения в хранилище и направляем пользователя на действие Index, так что он увидит моди­ фицированный список товаров. При наличии какой-нибудь проблемы с данными мы снова визуализируем стандартное представление, чтобы пользователь получил воз­ можность внести корректировки. После сохранения изменений в хранилище сообщение сохраняется с использо­ ванием средства TempDa ta. которое является частью средства состояния сеанса ASP.NET Саге. Это словарь пар "ключ/значение", похожий на применяемые ранее средства данных сеанса и ViewBag. Основное отличие объекта TempData от данных сеанса в том, что он хранится до тех пор. пока не будет прочитан. В такой ситуации использовать ViewBag невозможно. потому что объект ViewBag передает данные между контроллером и представлением, и он не может удерживать данные дольше, чем длится текущий НТТР-запрос. Когда редактирование успешно, браузер перенаправляется на новый URL, поэтому данные ViewBag утрачиваются. Мы могли бы прибегнуть к средству данных сеанса. но тогда сообщение хранилось бы вплоть до его явного удаления, чего делать бы не хотелось. Таким образом. объект TempDa ta подходит как нельзя лучше. Данные ограничи­ ваются сеансом одного пользователя (пользователи не видят объекты TempData друг друга) и хранятся достаточно долго, чтобы быть прочитанными. Мы будем читать данные в представлении, которое визуализируется методом действия, куда перена­ правлялся пользователь, и определяется в следующем разделе. Модульное тестирование: метод действия Edi t ( ) , обрабатывающий запросы POST В методе действия Edi t () , обрабатывающем запросы POST, мы должны удостоверить­ ся, что хранилищу товаров для сохранения передаются допустимые обновления объекта Product, полученного в качестве аргумента метода. Кроме того, необходимо проверить, что недопустимые обновления (т.е. содержащие ошибки проверки достоверности модели) в хранилище не передаются. Ниже приведены тестовые методы, которые добавлены в файл AdminControJlerTests. cs. [Fact] puЫic 11 void Сап Организация Save Valid Changes() { - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); 318 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 11 Организация - создание имитированных временных данных Mock<ITempDataDictionary> tempData = new Mock<ITempDataDictionary>(); //Организация - создание контроллера AdminController target = new AdminController (mock.Object) { TempData = tempData.Object ) ; 11 Организация - создание товара Product product = new Product { Name = "Test" } ; 11 Действие - попытка сохранить товар IActionResult result = target.Edit(product); //Утверждение - проверка того, что к хранилищу быnо произведено обращение mock.Verify(m => m.SaveProduct(product)); 11 Утверждение - проверка, что типом результата является nеренаправление Assert.IsType<RedirectToActionResult>(result); Assert.Equal ("Index", (result as RedirectToActionResult) .ActionName); [Fact] puЫic void Cannot_Save_Invalid_Changes() { - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); //Организация - создание контроллера AdminController target = new AdminController(mock.Object); 11 Организация - создание товара Product product = new Product { Name = "Test" ); // Организация - добавление ошибки в состояние модели target .ModelState .AddModelError ( "error", "error"); 11 Организация 11 Действие - IActioпResult попытка сохранить товар result = target.Edit(product); 11 Утверждение - проверка того, что к хранилищу быnо произведено обращение mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never()); 11 Утверждение - проверка типа результата метода Assert.IsType<ViewResult>(result); Отображение подтверждающего сообщения Мы будем иметь дело с сообщением, сохраненным с помощью компоновки _AdminLayout. cshtml (листинг 11.13). TempDa ta, в файле За счет обработки сообщения в компоновке мы можем создавать сообщения в любом представлении. которое применяет данную компоновку. без необходимости в создании дополнительных выражений Листинг 11.1 З. Razor. Обработка сообщения из папки TempDa ta в файле _AdminLayou t. csh tml Views/Shared <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> <title>@ViewBag.Title</title> </head> Глава 11. SportsStore: администрирование 319 <bod y c l as s= "m- 1 р - 1 " > <div c la ss= "bg - i nf o p- 2 " ><h 4 >@View8ag . Title< /h4 ></div> @if (TempData["message"] != null) { <div class="alert alert-success">@TempData [ "message" ]</div> @RenderBody () </bo dy> </ht ml > Совет. Преимущество работы с сообщением внутри компоновки заключается в том , что поль­ зователи будут видеть его на любой странице, визуализированной после сохранения из­ менений. В данный момент мы возвращаем его списку товаров, но рабочий поток можно изменить с целью визуализации какого-то другого представления, и пользователи будут по-прежнему видеть сообщение (при условии, что следующее представление использует ту же самую компоновку). Теперь мы располагаем всеми фрагментами для редактирования сведений о това­ рах. Чтобы увидеть. как все они работают, запустим приложение. перейдем на вида /Adrnin/Index. кнопке Save. щелкнем на кнопке Edit URL и внесем и з менение . Затем щелкнем на Произойдет перенаправле ние на /Adrn in /Index и отобразится сообще­ TempDa ta (ри с . 11 .5) . Если перезагруз ить страницу со списком товаров. то со ­ общение исчезнет. поскольку после чтения объект TempDat a удаляется . Подход очень ни е из удобен. т.к. не приходится иметь дело со старыми сообщениями. Х ~ EdtPJodi.кt Edit Product Name Green Kayak Pri" 2 lif•j•<k•t S•B.95 3 Soccer B•ll S19.50 S34.95 4 Corn<r Flags 5 Stadium S79,500.00 Th1nking Сар S16.00 7 Unste•dy Chaa S299S 8 Humon Choss 8cord S7S.OO б $1.200.00 9 Bling · Bling King 6 Рис. 11.5. дction1 $275.00 1 Grten Kayak Gil* Редактирование сведений о товаре и отображение сообщения из TempDa ta Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 320 Добавление проверки достоверности модели Мы добрались до точки, когда к илассам модели необходимо добавить правила проверки достоверности. Пока что администратор может вводить отрицательные значения для цен или оставлять описания пустыми и приложение SportsStoгe бла­ - гополучно сохранит эти данные в базе. Смогут ли недопустимые данные успешно сохраниться, зависит от того, удовлетворяют ли они ограничениям в таблицах созданных инфраструктурой Entity SQL, Fгamewoгk Соге, и для большинства приложений таких мер безопасности будет недостаточно. Чтобы защититься от недопустимых значений данных, свойства иласса делалось в илассе Листинг 11.14. Order Product 10 декорируются с помощью атрибутов, как (листинг 11.14). Применение атрибутов проверки достоверности в файле Product. cs из папки using using из главы Models System.Componentмodel.DataAnnotations; Мicrosoft.AspNetCore.Мvc.ModelBinding; namespace SportsStore.Models { puЫic class Product { int ProductID { get; set; } puЫic [Required(ErrorMessage = "Please enter 11 puЫic 11 а douЫe.мa.xValue, ErrorМessage = "Please 11 enter а positive price")] Введите положительное значение для цены decimal Price { get; set; } [Required(ErrorMessage = "Please specify 11 puЬlic В главе description")] Введите описание string Description { get; set; } [Required] [Range(0.01, puЫic product name")] string Name { get; set; } [Required(ErrorMessage = "Please enter puЫic а Введите наименование товара а category")] Укажите категорию string Category { get; set; } 1О использовалась вспомогательная функция дескриптора для отображе­ ния сводки по ошибкам проверки достоверности в верхней части формы. Здесь мы применим похожий подход, но будем отображать сообщения об ошибках рядом с эле­ ментами формы в представлении Листинг 11.15. Edi t (листинг 11. 15). Добавление элементов для отображения ошибок проверки достоверности в файле @model Product @{ ViewBag.Title = "Edit Product"; Layout = "_AdminLayout"; Edi t. cshtml из папки Views/ Admin Глава 11. SportsStore: администрирование 321 <form asp-action="Edit" method="post"> <input type="hidden" asp-for="ProductID" /> <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="Description"></l abel> <div><span asp-validation-for="Descri ption" class="text-danger"></span > </div> <textarea asp-for="Description" class="form-control"></te xtarea> </div> <div class="form-group"> <label asp-for="Category"></labe l> <div><span asp-validation-for="Categ ory" class="text-danger"></spa n> </div> <input asp-for="Category" class="form-control" /> </div> <div class="form-group"> <label asp-for="Price"></label> <div><span asp-validation-for="Price " class="text-danger"></spa n> </div> <input asp-for="Price" class="form-control" /> </div> <div class="text-center"> <button class="btn btn-primary" type="submit">Save</butto n> <а asp-action="Index" class="Ьtn Ьtn-secondary">Cancel</a> </div> </form> Когда атрибут asp-validation-for применяется к элементу span, он использу­ ет вспомогательную функцию дескриптора, которая добавляет сообщение об ошибке проверки достоверности для указанного свойства, если при проверке возникли какие­ то проблемы. Вспомогательные функции дескрипторов будут вставлять сообщение об ошибке в эле­ мент span и добавлять элемент в класс легко применять стили Листинг 11.16. CSS Добавление стиля из папки input-validation-error, который позволит к элементам с сообщениями об ошибках (листинг CSS в файле 11.16). _AdminLayout.cshtml Views/Shared < 1 DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-wid th" /> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> <title>@ViewBag.Title</t itle> <style> .input-validation-error { border-color: red; background-color: #fee </style> </head> 322 Часть 1. Введение в инфраструктуру ASP NET Core MVC 2 <body c l ass= "m-1 р-1" > <d iv cl a s s= "bg-info p - 2 " ><h4>@V ie wBa g.Tit le</h 4 >< /d i v > @if (TempData ["mess age "] 1 = null) ( <div c l ass= "alert alert -succe ss mt -1 ">@TempDa t a [ "mes sage " ] </ div> @Re nd e r Body () </body> </ html> Определенный здесь стиль са CSS выбирает элементы , i npu t- validation - error , которые являютс я членами кла с­ и устанавливает для них границу красного цвета и фон . Совет. Явная установка стилей, когда используется библиотека CSS наподобие Bootstrap , 27 может привести к несоответствиям в случае применения тем содержимого. В главе будет продемонстрирован альтернативный подход, при котором для применения классов Bootstrap к элементам с сообщениями об ошибках проверки JavaScript, сохраняя все в согласованном состоянии. достоверности используется код Вспомогательные функции дескрипторов для сообще ний проверки достоверно с ти можно применять где угодно в представлении, но по соглашению (что вполне разумно) принято размещать их поближе к пробл е мному элементу , чтобы вв ести пользователя в курс дела. На рис. 11 .6 показано, как выглядят сообщения об ошибках пров е рки достоверности и отобража е мые подсказки. для чего нужно запустить приложение. от ­ редактировать сведения о товаре и отправить недопустимые данны е. PIP-~ st> '°nter а product 11ame D~script ion Please e11ter а destпptюn J 1 Category 1llease spe,if) о cakgory 11 Price PI dse ~n\er а pos111ve pr<ce 1 -200 L Рис. 11.6. - Cancel Проверка достоверности данных при редактировании сведений о товаре Глава 11. SportsStore: администрирование 323 Включение проверки достоверности на стороне клиента В текущий момент проверка достоверности данных применяется, только когда пользователь-администратор отправляет результаты редактирования серверу. но большинство пользователей ожидают немедленного отклика при наличии проблем с введенными данными. Именно потому разработчики часто предпочитают выполнять проверку достоверности на стороне клиента, при которой данные проверяются в браузере с использованием JavaScript. Приложения МVС могут выполнять провер­ ку достоверности на стороне клиента на основе аннотаций данных, применяемых к классу модели предметной области. Прежде всего, понадобится добавить библиотеки JavaScript, которые предоставят приложению средство проверки достоверности на стороне клиента, файле bower. j son Листинг 11.17. (листинг 11.17). Добавление пакетов из папки что делается в JavaScript SportsStore в файле bower. j son "name 11 : "asp.net", "private": true, "dependencies" : { "bootstrap": "4.0.0-alpha.6", "fontawesome": "4.7.0", "jquery": "3.2.1", "jquery-validation": "1.17. 0", "jquery-validation-unobtr usive": "3.2.6" Проверка достоверности на стороне клиента построена на основе популярной библиотеки jQuery, которая упрощает работу с АРI-интерфейсом Следующий шаг связан с добавлением файлов JavaScript в DOM гружались, когда используются средства администрирования приложения (листинг Листинг браузера. компоновку. чтобы они за­ SportsStore 11. 18). 11. 18. Добавление библиотек проверки достоверности в файле _Ad.minLayout. cshtml из папки Views/Shared < 1 DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-wid th" /> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> <title>@ViewBag.Title</t itle> <style> .input-validation-error { border-color: red; background-color: #fee </style> <script src="/lib/jquery/dist/jque ry.min.js"></script> <script src="/lib/jquery-validati on/dist/jquery.validate.m in.js"> </script> <script src="/liЬ/jquery-validation-unobtrusive/jquery.validate.unoЬtrusive.min.js"> </script> </head> 324 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 <body class="m-l р-1"> <div class="bg-info p-2"><h4>@ViewBag.Title</h4></div> @if (TempData["message"] != null) { <div class="alert alert-success mt-1 ">@TempDat.a [ "message"] </div> @RenderBody () </body> </html> Включение проверки достоверности на стороне клиента не приводит к каким-то визуальным изменениям. Но ограничения. которые указаны с помощью атрибутов. примененных к классу модели С#, вступают в силу на уровне браузера, предотвращая отправку пользователем формы с недопустимыми данными и обеспечивая немедлен­ ный отклик при наличии проблемы. За дополнительными сведениями обращайтесь в главу 27. Создание новых товаров Далее мы реализуем метод действия Product Crea te (), который указан для кнопки Add на главной странице со списком товаров. Он позволит администратору добав­ лять новые элементы в каталог товаров. Добавление возможности создания новых това­ ров требует одного небольшого дополнения в приложении, что является великолепной демонстрацией мощи и гибкости хорошо структурированного приложения чала добавим в контроллер Листинг 11.19. Adrnin метод Crea te () , Добавление метода действия в файле Crea te () AdminController. cs из папки Controllers 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); ViewResult Edit(int productid) => View(repository.Products .FirstOrDefault(p => p.ProductID == productid)); puЫic [HttpPost] IActionResult Edit(Product product) if (ModelState. IsValid) { repository.SaveProduct(product); TempData["message"J = $"{product.Namef has been saved"; return RedirectToAction("Index"); else { puЬlic 11 Что-то не так со значениями данных return View(product); MVC. Для 11.19. как показано в листинге на­ Глава 11. SportsStore: администрирование puЬlic => View ( "Edi t", new Product ()) ; ViewResul t Create () Crea te ( ) Метод 325 не визуализирует свое стандартное представление . Взамен он указывает, что должно использоваться представление Edi t. Применение в методе действия представления. которое обычно связано с другим методом действия, впол­ не допустимо . В рассматриваемом случае мы указываем в качестве модели представ ­ ления новый объект Product. так что представление заполняется пустыми Edi t полями. На заметку! Модульный тест для метода действия Crea t е () не добавляется. Он позволил бы проверить только способность обработки инфраструктурой тата, возвращаемого методом действия - ASP.NET Core MVC резуль­ то, что мы считаем само собой разумеющим­ ся. (Обычно тесты для функциональных средств инфраструктуры не пишутся, если только нет подозрения о наличии дефекта.) Это единственное изменение. которое потребовалось внести. поскольку метод действия Edi t () уже настроен на получение объектов Product от системы привязки моделей и на их сохранение в базе данных. Чтобы протестировать имеющуюся фун ­ кциональность. запустим приложение , перейдем на нем на кнопке Add Product, URL вида / Adrni n/Index, щелк­ заполним форму и отправим ее. Информация, введенная на форме, будет использоваться для создания нового товара в базе данных, который затем появится в списке (рис. 11. 7). Description Breгithe 10 Nam• underwater Price S275.00 1 Green Kayak 1 Cate901y Wate1'Sports Price 2 Lifejack~t З Soccer Ball S19.SO 4 Corner Ffags SЗ4.95 S Stadium 10 50 S48.95 579.500.00 6 Thi11kin9 Сар " 1 $16.00 Unsteьdy Cha1r 52995 8 Human Ch~s Шrd 9 Bling·Bling King S7S.OO SJ.200.00 10 Snorkel 51050 *0111 Рис. 11. 7. Добавление нового товара в каталог Actio.ns 326 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 Удаление товаров Обеспечить поддержку удаления элементов из каталога довольно просто. Для на­ чала добавим в интерфейс тинге IProductRepository новый метод, как показано в лис­ 11.20. Листинг 11.20. Добавление метода для удаления товаров в файле IProductReposi tory. cs из папки Models using System.Linq; namespace SportsStore.Models puЫic interface IProductRepository IQueryaЬle<Product> Products { get; void SaveProduct(Product product); Product DeleteProduct (int productID) ; Затем реализуем метод Core, DeleteProduct () в классе EFProductRepository (листинг 11.21). т.е. Листинг 11.21. хранилища Реализация поддержки удаления в файле из папки Entity Framework EFProductReposi tory. cs Model s using System.Collections.Generic; using System.Linq; namespace SportsStore.Models ( class EFProductRepository : IProductRepository ( private ApplicationDbContext context; puЫic EFProductRepository(ApplicationDbContext ctx) ( context = ctx; puЬlic puЬlic IQueryaЫe<Product> Products => context.Products; void SaveProduct(Product product) i f ( product. Product I D == О) ( context.Products.Add(product); else ( Product dbEntry = context.Products .FirstOrDefault(p => p.ProductID product.ProductID); if (dbEntry != null) { dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; dbEntry.Category = product.Category; puЫic context.SaveChanges(); Глава 11. SportsStore: администрирование 327 Product DeleteProduct(int productID) ( Product dЬEntry = context. Products .FirstOrDefault(p => p.ProductID == productID); puЫic (dЬEntry if != null) ( context.Products.Reшove(dЬEntry); context.SaveChanges(); return dЬEntry; Финальный шаг связан с реализацией метода действия Admin. Метод действия Delete () Delete () в контроллере должен поддерживать только запросы POST, пото­ му что удаление объектов не является идемпотентной операцией. Как будет показа­ но в главе 16, браузеры и кеши вольны выдавать запросы GET без явного согласия пользователя, поэтому мы должны проявить осторожность, чтобы избежать внесе­ ния изменений как следствия запросов листинге Листинг GET. Код нового метода действия приведен в 11.22. 11.22. Добавление метода действия Delete () в файле AdminController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; namespace SportsStore.Controllers puЫic class AdminController : Controller private IProductRepository repository; AdminController(IProductRepository repo) { repository = repo; puЫic puЫic ViewResult Index() => View(repository.Products); ViewResult Edit(int productid) => View(repository.Products .FirstOrDefault(p => p.ProductID == productid)); puЫic [HttpPost] IActionResult Edit(Product product) puЫic if (ModelState. IsValid) { repository.SaveProduct(product); TempData["message"] = $"{product.Name) has been saved"; retur·n RedirectToAction ( "Index"); else { 11 Что-то не так со значениями данных return View(product); Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 328 puЫic IActionResult Create() => View("Edit", new Product()); [HttpPost] IActionResult Delete (int productid) Product deletedProduct = repository.DeleteProduct(productid); if (deletedProduct != null) { TempData["message"] = $ 11 {deletedProduct.Name} was deleted"; puЫic return RedirectToAction (" Index") ; Модульное тестирование: удаление товаров Нам нужно протестировать основное поведение метода действия Delete (), которое за­ ключается в том, что при передаче в качестве параметра допустимого идентификатора ProductID метод действия должен вызвать метод редать ему корректное значение Р roduct I файл DeleteProduct () хранилища и пе­ D удаляемого товара. Вот тест, добавленный в AdminControllerTests. cs: [ Fact] void Can Delete Valid Products() ( 11 Организация - создание объекта Product Product prod = new Product { ProductID = 2, Name = "Test" } ; puЫic 11 Организация - создание имитированного хранилища Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products) .Returns( (new Product[) { 1, Name "Pl"}, new Product (ProductID prod, "РЗ"}, new Product (ProductID 3, Name )) .AsQueryaЫe<Product>()); //Организация - создание контроллера AdminController target = new AdminController(mock.Object); 11 Действие - удаление товара target.Delete(prod.ProductID); // Утверждение - проверка того, что был вызван метод удаления 11 в хранилище с корректным объектом Product mock.Verify(m => m.DeleteProduct(prod.ProductID)); Чтобы увидеть средство удаления в работе, запустим приложение, перейдем на URL вида /Admin/ Index и щелкнем на одной из кнопок ницы со списком товаров (рис. ременной TempData l l .8). Delete (Удалить) внутри стра­ На рисунке можно заметить, что с помощью пе­ отображается сообщение об удалении товара из каталога. Глава 11 . SportsStore : администрирование ~ All Produc11 329 х All Products 10 Nomo Snon:et ""as deletcd S275.00 1 Green l<ayak 2 Lilejacket $48.95 3 Soccer Boll S19.50 Prict Actlons 5275.00 1 Green К.уаk S34.95 4 Corner Flags 2 Lifej•cket $4895 3 Soccer в.11 S19.50 $79 50000 5 Su.dium 6 Th1nk1n9 Сар S16.00 7 Unsteady Choir S29.95 534.95 4 Corner Flags S79.S00.00 S7S.OO 8 Human Chess Bcard S16.00 Sl.200.00 9 Bl1ng·Bling King 52995 $10.50 10 Snorkel $75.00 8 Human Chess 8oard •111u $1,lOO.OO 9 Bkng·Bling K1ng *ФО#* Рис. 11.8. Удаление товара из каталога На заметку! При попытке удалить товар , для которого ранее был создан заказ, возникнет ошибка. Когда объект Or de r сохраняется в базе данных , он превращается в запись внут­ ри таблицы базы данных, которая содержит ссылку на связанный с ним объект Produ ct , что известно как отношение внешнего ключа. Это означает, что по умолчанию база дан ­ ных не позволит удалить объект Pro d uc t, если для него был создан объект Order, поскольку такое действие внесло бы несогласованность в базу данных . Существует не­ Ord e r при Pr odu ct , к которому они относятся , либо изменение отношения между Prod uct и Order. Подробные сведения можно найти в документации Eпtity сколько способов решения указанной проблемы, включая удаление объектов удалении объекта объектами Framework Core. Резюме В главе были введены средства администрирования и показано. как реализовать операции CRUD. которы е позволяют администратору со здавать. читать , обновлять и удалять т ова ры из хранил ища и пом ечать заказы ка к отгруже нны е . В следующей главе мы продем он с трируе м с п о соб з а щиты админи ст р ативных функций. чтобы они не были доступными абсолютно всем пользовател ям, и разве рн ем приложение SportsStore в производственной среде. ГЛАВА 12 SportsStore: защита и развертывание в предыдущей главе мы добавили в приложение SportsStoгe поддержку админис­ трирования. и от вашего внимания, вероятно, не ускользнул тот факт, что если развернуть приложение в том виде как есть, то модифицировать каталог товаров смо­ жет любой пользователь. Для этого ему лишь нужно знать. что средства администри­ рования доступны через URL вида /Admin/ Index и /Order /List. В настоящей главе мы покажем, как предотвратить использование административных функций случай­ ными посетителями, защитив их паролем. После добавления возможности защиты мы объясним, каким образом подготовить и развернуть приложение SpoгtsStoгe в производственной среде. Защита средств администрирования ASP.NET Саге Jdeпtity. ASP.NET Саге, так и в приложения Аутентификация и авторизация предоставляются системой которая аккуратно интегрируется как в платформу MVC. В последующих разделах мы создадим базовую настройку защиты, которая поз­ волит одному пользователю по имени Admin проходить аутентификацию и получать доступ к административным функциям в приложении. Система ASP.NET Саге Ideпtity предлагает множество других средств для аутентификации пользователей, а также авторизации доступа к функциям и данным приложения. Более подробные сведения вы найдете в главах 28-30, где будет показано, как создавать и управлять пользова­ тельскими учетными записями, каким образом применять роли и политики и как поддерживать аутентификацию от третьих сторон вроде и Twitter. Однако цель текущей главы - Microsoft, Google, Facebook создать лишь столько функциональности. сколько достаточно для предотвращения доступа пользователей к чувствительным частям приложения SportsStore, что будет содействовать пониманию того, каким об­ разом аутентификация и авторизация вписываются в приложение Создание базы данных Система ASP.NET Core MVC. Iden ti ty Jdeпtity чрезвычайно конфигурируема и расширяема, под­ держивая многочисленные варианты хранения данных о пользователях. Мы собира­ емся использовать наиболее распространенный вариант, который предусматривает хранение данных с применением Eпtity Framework Core. Microsoft SQL Server и доступ к ним с помощью 331 Глава 12. SportsStore: защита и развертывание Создание класса контекста Нам необходимо создать файл контекста базы данных, который будет действовать в качестве шлюза между базой данных и объектами моделей базы данных предоставляющими к ней доступ. Добавим в папку Appldenti tyDbContext. cs Models Identi ty, файл класса по имени с определением, приведенным в листинге 12. l. На заметку! Вы могли привыкнуть к тому, что для получения дополнительных функциональных средств вроде защиты к проекту необходимо добавлять пакеты. Но с выпуском Core 2 пакеты NuGet, пакет, который был добавлен в файл Листинг 12.1. ASP.NET требующиеся для системы ldeпtity, уже включены в проект через мета­ Содержимое файла SportsStore. csproj как часть шаблона проекта. AppidentityDbContext.cs из папки Models using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore ; using Microsoft.EntityFrameworkCore; namespace SportsStore.Models { puЫic class AppidentityDbContext : IdentityDbContext<IdentityUser> { puЫic AppidentityDbContext(DbContextOptions<AppidentityDbCo ntext> options) : base(options) { ) Класс Appldenti tyDbContext является производным от класса Identi tyDbContext, который предлагает связанные с раметре типа используется ldentity средства для Identi tyuser - представления пользователей. В главе 28 Entity Framework Core. В па­ встроенный класс, применяемый для будет продемонстрировано использование специального класса, который можно расширять с целью добавления дополнительной информации о пользователях приложения. Определение строки подключения Далее определяется строка подключения, предназначенная для базы данных. В лис­ тинге 12.2 показано добавление в файл appsettings. j son проекта SportsStore, которое следует тому же самому формату. что и строка подключения, определенная для базы данных товаров в главе Листинг 12.2. 8. Определение строки подключения в файле из папки appsettings. json SportsStore "Data": { "SportStoreProducts": "ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=SportsS tore; Trusted_Connection=True;MultipleActiveResultSets =true" ) ' "SportStoreidenti ty" : { "ConnectionStrinq": "Server=(localdЬ)\\МSSQLLocalDB;DataЬase=Identity; Trusted_Connection=True;МultipleActiveResultSets=true" Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 332 Вспомните, что строка подключения должна определяться в файле j son appsettings. как единственная неразрывная строка кода. а в листинге она выглядит так из-за ограниченной ширины печатной страницы. Добавленная разметка определяет строку подключения по имени Loca!DB под названием SportsStoreidenti ty, Identi ty. в которой указывается база данных Конфигурирование приложения Подобно другим средствам ASP.NET Core система ldentity конфигурируется в Start. В листинге 12.3 приведены добавления для настройки Identity в про­ SportsStore с применением ранее определенного класса контекста и строки классе екте подключения. Листинг 12.З. Конфигурирование средства ldeпtity в файле из папки using using using using using using using using using using using using Startup. cs SportsStore System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; SportsStore.Models; Microsoft.Extensions.Configuration; Microsoft.EntityframeworkCore; Мicrosoft.AspNetCore.Identity; namespace SportsStore { puЬlic class Startup { Startup(IConfiguration configuration) => Configuration = configuration; puЬlic puЫic IConfiguration Configuration { get; ) void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreProducts:ConnectionString"J)); puЫic services.AddDbContext<AppidentityDbContext>(options => options.UseSqlServer( Configuration["Data:SportStoreidentity:ConnectionString"])); services.Addidentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<AppidentityDbContext>() .AddDefaultTokenProviders(); 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(); 333 Глава 12. SportsStore: защита и развертывание void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseSession(); app.UseAuthentication(); app.UseMvc(routes => { puЫic 11 ... для арр, IHostingEnvironment env) { краткости маршруты не показаны ... ) ) ; SeedData.EnsurePopulated(app); В методе конфигурация ConfigureServices () Entity для регистрации класса контекста и с помощью метода вает службы Identity. Fгamewoгk Соге расширена Addidenti ty () устанавли­ используя встроенные классы для представления пользователей и ролей. Внутри метода Configure () вызывается метод UseAuthentication () для установки компонентов, которые будут перехватывать запросы и ответы для внедре­ ния политики безопасности. Соэдание и применение миграции базы данных Основная конфигурация на месте, и самое время воспользоваться средством миг­ раций Entlty Fгamewoгk Соге для определения схемы и ее применения к базе данных. Откроем окно командной строки или PoweгShell и введем показанную ниже коман­ д.У в папке проекта SportsStore, чтобы создать новую миграцию для базы данных Identity: dotnet ef migrations add Initial --context AppidentityDbContext Существенное отличие от предшествующих команд базы данных связано с тем. что здесь используется аргумент --context текста, ассоциированного с базой данных, AppidentityDbContext. для указания имени класса кон­ с которой необходимо работать - При наличии множества баз данных в приложении важно гарантировать работу с правильным классом контекста. После того как инфраструктура Entlty Fгamewoгk Соге сгенерировала начальную миграцию, выполним следующую команду. чтобы создать базу данных и запустить команды миграции: dotnet ef database update --context AppidentityDbContext Результатом будет новая база данных LocalDB по имени но просмотреть с применением проводника объектов Ехрlогег) среды SQL Identity, Serveг которую мож­ (SQL Serveг Object Visual Studio. Определение начальных данных Мы планируем явно создать пользователя Admin, наполняя базу данных началь­ ными данными при запуске приложения. Добавим в папку имени IdentitySeedData. cs Models файл класса по и определим в нем статический класс (листинг 12.4). 334 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 12.4. Содержимое файла Iden ti tySeedDa ta. cs из папки Model s using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Dependencyinjection; namespace SportsStore.Models { puЫic static class IdentitySeedData private const string adminUser = "Admin"; private const string adminPassword = "Secret123$"; puЫic static async void EnsurePopulated(IApplicationBuilder арр) UserManager<IdentityUser> userManager = app.ApplicationServices .GetRequiredService<UserManager<IdentityUser>>(); IdentityUser user = await userManager.FindByidAsync(adminUser); if (user == null) { user = new IdentityUser("Admin"); await userManager.CreateAsync(user, adminPassword); В коде используется класс UserManager<T>. который система ASP.NET Core Identity предоставляет в виде службы для управления пользователями, как обсуждается в гла­ ве 28. В базе данных производится поиск учетной записи пользователя рая в случае ее отсутствия создается (с паролем Secret123$). закодированный пароль в этом примере, поскольку система Admin, кото­ Не изменяйте жестко Identity имеет политику проверки достоверности, которая требует, чтобы пароли содержали цифры и диапа­ зон символов. Способ изменения настроек. относящихся к проверке достоверности. описан в главе 28. Внимание! Жесткое кодирование деталей учетной записи администратора часто требуется для того, чтобы можно было войти в приложение после его развертывания и начать ад­ министрирование. Поступая так, вы должны помнить о необходимости изменения пароля для учетной записи, которую создали. В главе 28 приведены детали того, как изменять пароли, используя ldeпtity. Чтобы обеспечить начальное заполнение базы данных приложения. добавим в метод в листинге Листинг класса I den t i t у во время запуска Startup оператор. как показано 12.5. 12.5. Начальное заполнение базы данных из папки puЫic Configure () Identi ty в файле Startup. cs SportsStore void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseSession(); арр, IHostingEnvironment env) { Глава 12. SportsStore: защита и развертывание 335 app.UseAuthentication(); app.UseMvc(routes => ( 11 ... для краткости маршруты не показаны ... ) ) ; SeedData.EnsurePopulated (app); IdentitySeedData.EnsurePo pulated(app); Применение базовой политики авторизации Теперь, когда система ASP.NET Core Identity установлена и сконфигурирована, можно применить политику авторизации к тем частям приложения. которые необхо­ димо защитить. Мы собираемся использовать самую базовую политику авторизации. которая предусматривает разрешение доступа любому пользователю, прошедшему аутентификацию. Хотя она может оказаться полезной политикой также и в реаль­ ном приложении, существуют возможности для создания более детализированных элементов управления авторизацией (как описано в главах в приложении SportsStore 28-30), но из-за того, что имеется только один пользователь, вполне достаточно про­ вести различие между анонимными и аутентифицированными запросами. Атрибут в листинге Authorize применяется для ограничения доступа к методам действий, и 12.6 видно, что этот атрибут используется для защиты доступа к админис­ тративным действиям в контроллере Order. Листинг 12.б. Ограничение доступа в файле из папки OrderController. cs Controllers using Microsoft.AspNetCore.Mvc ; using SportsStore.Models; using System.Linq; using Мicrosoft.AspNetCore.Authorization; namespace SportsStore.Controllers ( class OrderController : Controller private IOrderRepository repository; private Cart cart; puЫic OrderController(IOrderRe pository repoService, Cart cartService) ( repository = repoService; cart = cartService; puЫic [Authorize] ViewResult List() => View(repository.Orders.W here(o => puЫic 1 o.Shipped) [HttpPost] [Authorize] IActionResult MarkShipped(int orderID) ( Order order = repository.Orders .FirstOrDefault(o => o.OrderID == orderID); puЫic ); 336 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 if (order ! = null) { order.Shipped = true; repository.SaveOrder(order); return RedirectToAction(nameof(List)); puЫic ViewResult Checkout() => View(new Order()); [HttpPost] puЫic IActionResult Checkout(Order order) { if (cart.Lines.Count() == 0) { 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(); Мы не хотим препятствовать доступу пользователей, не прошедших аутенти­ фикацию, к остальным методам действий в контроллере Authorize применен только к методам тить все методы действий. определяемые счет применения атрибута Authorize Order, так что атрибут MarkShipped ().Нам нужно защи­ контроллером Admin, чего можно достичь за List () и к самому классу контроллера, что приведет к применению политики авторизации ко всем содержащимся в нем методам действий (листинг Листинг 12.7). 12. 7. Ограничение доступа в файле из папки AdminCon troller. cs Controllers using Microsoft.AspNetCore.Mvc; using SportsStore.Models; using System.Linq; using Мicrosoft.AspNetCore.Authorization; namespace SportsStore.Controllers { [Authorize] puЫic class AdminController : Controller private IProductRepository repository; puЫic AdminController(IProductRepository repo) { repository = repo; Глава 12. SportsStore: защита и развертывание puЬlic 337 ViewResult Index() => View(repository.Products); ViewResult Edit(int productid) => View(repository.Products . FirstOrDefaul t (р => р. ProductID == productid) ) ; puЫic [HttpPost] IActionResult Edit(Product product) i f (ModelState. IsValid) ( repository.SaveProduct(product); TempData["message"] = $"(product.Name} has been saved"; return RedirectToAction("Index"); else ( puЬlic //Что-то не так со значениями данных return View(product); puЬlic ViewResult Create() => View("Edit", new Product()); [HttpPost] puЬlic IActionResult Delete(int productid) Product deletedProduct = repository.DeleteProduct(productid); if (deletedProduct != null) ( TempData["message"] = $"(deletedProduct.Name} was deleted"; return RedirectToAction("Index"); Создание контроллера Accoun t и представлений Когда пользователь, не прошедший аутентификацию, посылает запрос, который требует авторизации, он перенаправляется на URL вида /Account/Login, где прило­ жение может предложить пользователю ввести свои учетные данные. В качестве под­ готовки создадим модель представления для учетных данных пользователя, добавив в папку Models/ViewModels показанным в листинге Листинг 12.8. файл класса по имени LoginModel. cs Содержимое файла LoginМodel. cs из папки using System.ComponentModel.DataAnnotations; namespace SportsStore.Models.ViewModels puЫic class LoginModel ( [Required] puЫic string Name ( get; set; } [Required] [UIHint("password")] puЫic string Password ( get; set; } puЫic с определением, 12.8. string ReturnUrl ( get; set; } "/"; Models/ViewModels Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 338 Свойства Name и Password декорированы атрибугом Required, который исполь­ зует проверку достоверности модели для обеспечения того, что значения были пре­ доставлены. Свойство применения атрибута Password декорировано атрибутом UIHint, поэтому в случае asp-for внутри элемента input представления Razoг, пред­ назначенного для входа. вспомогательная функция дескриптора установит атрибут type в password; таким образом, вводимый пользователем текст не будет виден на экране. Использование атрибуга Далее добавим в папку UIHint описано в главе 24. Controllers файл класса по имени AccountController. cs с определением контроллера. приведенным в листинге дет отвечать на запросы к Листинг using using using using using using 12.9. 12.9. Данный контроллер бу­ URL вида /Account/Login. Содержимое файла AccountController. cs из папки Controllers System.Threading.Tasks; Microsoft.AspNetCore.Authorization; Microsoft.AspNetCore.Identity; Microsoft.AspNetCore.Identity.EntityFrameworkCore; Microsoft.AspNetCore.Mvc; SportsStore.Models.ViewModels; namespace SportsStore.Controllers [Authorize] class AccountController : Controller private UserManager<IdentityUser> userManager; private SigninManager<IdentityUser> signinManager; puЬlic AccountController(UserManager<IdentityUser> userMgr, SigninManager<IdentityUser> signinMgr) { userManager = userMgr; signinManager = signinMgr; puЫic [AllowAnonymous) ViewResult Login(string returnUrl) { return View(new LoginModel ReturnUrl = returnUrl puЫic } ) ; [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken) puЫic async Task<IActionResult> Login(LoginModel loginModel) { if (ModelState. IsValid) { IdentityUser user = await userManager.FindByNameAsync(loginModel.Name); if (user '= null) { await signinManager.SignOutAsync(); if ( (await signinManager.PasswordSigninAsync(user, loginModel.Password, false, false)) .Succeeded) return Redirect(loginModel?.ReturnUrl ?? "/Admin/Index"); Глава 12. SportsStore: защита и развертывание 339 ModelState.AddModelError ("", "Invalid name or password"); 11 Неправильное имя или пароль return View(loginModel); "/") { async Task<RedirectResult> Logout(string returnUrl await signinManager.SignOutAsy nc(); return Redirect(returnUrl); puЫic Когда пользователь перенаправляется на метода действия Login () URL вида /Account/Login, версия GET визуализирует стандартное представление для страницы и создает объект модели представления, включающий URL, на который браузер должен быть перенаправлен, если запрос на аутентификацию завершился успешно. Учетные данные аутентификации отправляются версии РО S т метода дейс­ Login (), которая применяет службы UserManager<Identi tyUser> и SigninManager<Identi tyUser>, полученные через конструктор класса контрол­ твия лера, для аутентификации пользователя и его входа в систему. Работа упомянутых классов объясняется в главах 28-30, а пока достаточно знать, что в случае отказа в аутентификации создается ошибка проверки достоверности модели и визуализиру­ ется стандартное представление. Если же аутентификация прошла успешно, тогда пользователь перенаправляется на URL, к которому он хотел получить доступ перед тем, как ему было предложено ввести свои учетные данные. Внимание! В целом использование проверки достоверности данных на стороне клиента яв­ ляется хорошей практикой. Она освобождает от определенной работы сервер и обеспе­ чивает пользователям немедленный отклик о предоставленных ими данных. Тем не менее, не поддавайтесь искушению выполнять на стороне клиента аутентификацию, поскольку обычно это предусматривает передачу клиенту допустимых учетных данных, которые бу­ дут применяться при проверке вводимых имени пользователя и пароля, или, по меньшей мере, наличие доверия сообщению клиента о том, что аутентификация завершилась ус­ пешно. Аутентификация должна всегда выполняться на сервере. Чтобы снабдить метод Views/Account Login () представлением для визуализации, создадим папку и поместим в нее файл представления Razoг по имени с содержимым, показанным в листинге Листинг 12.10. Содержимое файла Login. cshtml 12.10. Login.cshtm.l из папки Views/Account @model LoginModel @{ ViewBag.Title = "Log In"; Layout =" AdminLayout"; <div class="text-danger" asp-validation-summary="A ll"></div> <form asp-action="Login" asp-controller="Account" method="post"> <input type="hidden" asp-for="ReturnUrl" /> <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> 340 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 <div class="form-group"> <label asp-for="Password"></label> <div><span asp-validation-for="Password" class="text-danger"></span> </div> <input asp-for="Password" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Log In</button> </form> Финальный шаг связан с изменением разделяемой компоновки для админис­ трирования, чтобы добавить кнопку Log Out (Выход), которая позволит текущему пользователю выходить из приложения за счет отправки запроса действию (листинг 12.11). Logou t Это удобное средство, облегчающее тестирование приложения, без которого пришлось бы очищать сооkiе-наборы браузера, чтобы возвращаться в состо­ яние, когда аутентификация еще не прошла. Листинг 12.11. Добавление кнопки из папки Log Out Views/Shared в файле_AdminLayout. cshtml <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-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-color: #fee; </style> <script src="/liЬ/jquery/dist/jquery.min.js"></script> <script src="/liЬ/jquery-validation/dist/jquery.validate.min.js"> </script> <script src= "/liЬ/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"> </script> </head> <body class="m-1 р-1"> <div class="bg-inf'o р-2 row"> <div class="col"> <h4>@ViewBag.Title</h4> </div> <div class="col-2"> <а class="btn btn-sm Ьtn-primary" asp-action="Logout" asp-controller="Account">Log Out</a> </div> </div> @if (TempData["message"] != null) <div class="a1ert alert-success mt-l">@TempData["message"]</div> @RenderBody () </body> </html> Глава 12. SportsStore : защита и развертывание 341 Тестирование политики безопасности Теперь можно протестировать политику безопасности, запустив приложение и запросив URL вида / Admin/Index . Поскольку в настоящий момент мы еще не про­ шли аутентификацию и пытаемся обратиться к действию, которое требует автори­ зации, браузер будет перенаправлен на и Secret123$ URL вида / Account/Login. Введем в качестве имени и пароля и отправим форму. Контроллер Admin Account сравнит предоставленные учетные данные с начальными данными . добавленными в базу данных I den t i t у, и (при условии ввода правильных сведений) аутентифицирует нас. после чего перенаправит обратно на ступ. Процесс проиллюстрирован tia рис. / Accoun t /I,og in. 12.1. $275.00 1 Green Kayak 2 Lifejacket $48.95 3 5occer Ball $19.50 4 Comer Fl•gs J Рис. 12.1. куда теперь имеется до­ $34.95 5 Stadium б $79.500.00 .,., 1hinking Сар S16.00 ,,. ~ Процесс аутентификации/авторизации для административных функций Развертывание приложения Все средства и функциональность приложения SportsStore на месте . так что на­ ступил момент для его подготовки и развертывания в производственной среде. В от­ ASP.NET Саге MVC доступно множество вариантов размещения . - платформа Microsoft Azure. Она вы­ брана из-за того. что поступает от Microsoft и предлагает бесплатные учетные записи , т.е. вы можете полностью проработать пример SportsStore. даже если не хотите при­ менять Azure в собственных проектах. ношении приложений и в настоящей главе используется один из них На заметку! В данном разделе понадобится учетная запись можете создать бесплатную учетную за п ись на Azure. Если у вас ее пока нет. тогда h t tps : / / a z ure . mi c rosof t. с от/. Создание баз данных Начальной задачей является создание баз данных. которые приложение SportsStore будет использовать в производственной среде . Такую задачу можно выполнять как часть процесса разработки в Visua l Studio. но трудность ситуации в том. что нужно 342 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 знать строки подключений для баз данных до развертывания, а это относится к про­ цессу. который создает базы данных. Внимание! Портал Azure часто меняется по мере того, как в Microsoft добавляют новые средства и пересматривают существующие. Инструкции, приводимые в настоящем раз­ деле, были точными на время его написания, но к моменту выхода книги необходимые шаги могут слегка измениться. Базовый подход должен остаться таким же, но имена по­ лей данных и точный порядок шагов может потребовать определенного экспериментиро­ вания, чтобы добиться правильных результатов. Самый простой подход предусматривает вход на портал com https: / /portal. azure. Azure и создание баз данных вручную. SQL Databases (Базы данных SQL) и щелк­ с применением своей учетной записи После входа выберем категорию ресурсов нем на кнопке Add (Добавить), чтобы создать новую базу данных. Для первой базы данных укажем имя products. Щелкнем на ссылке Configure Required Settings (Конфигурировать обязательные настройки) и затем на ссылке Create а New Server (Создать новый сервер). Введем имя нового сервера, которое должно быть уникальным в рамках Azure, и выберем имя пользователя и пароль для администрато­ ра. В рассматриваемом примере было указано имя сервера sportsstorecore2dЬ, имя пользователя sportsstoreadmin и пароль Вам понадобится использо­ Secret123$. вать другое имя сервера и сформировать более надежный пароль. Выберем местопо­ ложение для базы данных и щелкнем на кнопке ОК, чтобы закрыть экран параметров. и далее на кнопке Create (Создать), чтобы создать саму базу данных. Порталу Azure потребуется несколько минут для выполнения процесса создания, после чего база данных появится в категории ресурсов SQL Databases. SQL. указав Создадим еще один сервер баз данных на этот раз имя identity. Вместо создания нового сервера баз данных можно применять тот, который был со­ здан ранее. Результатом окажутся две базы данных детали которых приведены в табл. 12. l. SQL Server, размещаемые Azure, У вас будут другие имена серверов баз дан­ ных и наверняка более надежные пароли. Таблица 12. 1. Базы данных Azure дпя приложения SportsStore Имя пользователя- Имя базы данных Имя сервера products sportsstorecore2dЬ sportsstoreadmin Secretl23$ identity sportsstorecore2dЬ sportsstoreadmin Secretl23$ Пароль администратора Открь1тие доступа в брандмауэре для конфигурирования Далее необходимо создать схемы баз данных, и сделать это проще всего, открыв доступ в брандмауэре Core Azure, чтобы можно было запускать команды Entity Framework из машины разработки. Выберем одну их двух баз данных в категории ресурсов SQL Databases, щелкнем на Open in Visual Studio (Открыть в Visual Studio). Теперь щелкнем на ссылке Configure Your Firewall (Конфигурировать брандмауэр). щелкнем на кнопке Add Client IP (Добавить IР-адрес клиента) и щелкнем на кнопке Save (Сохранить). В результате текущий IР-адрес получит возможность достигать кнопке Tools (Сервис) и затем щелкнем на ссылке сервера баз данных и выполнять команды конфигурирования. (Проинспектировать схему базы данных можно, щелкнув на кнопке Open ln Visual Studio, чтобы открыть Глава 12. SportsStore: защита и развертывание Visual Studio и воспользоваться окном SQL Server Object Explorer 343 для исследования базы данных.) получение строк подключений Вскоре понадобятся строки подключений для новых баз данных. Портал Azure предоставляет такую информацию по щелчку на базе данных в категории ресурсов SQL Databases через ссылку Show Database Connection Strings (Показать строки под­ ключений для баз данных). Строки подключений предлагаются для разных платформ разработки; приложениям которую портал Azure .NET требуются строки ADO.NET. Вот строка products: подключения, предоставляет для базы данных Server=tcp:sportsstorecore2db.database.windows.ne t,1433; Initial Catalog=products; Persist Security Info=False; User ID={ваше_имя_попьзоватепя};Раsswоrd={ваш_паропь}; MultipleActiveResultSets=True;Encrypt=True; TrustServerCertificate=False;Connection Timeout=ЗO; В зависимости от того, как портал Azure подготовил базу данных, вы будете видеть разные параметры конфигурации. Обратите внимание на выделенные полужирным заполнители для имени пользователя и пароля. которые должны быть изменены, ког­ да вы применяете строку подключения при конфигурировании приложения. Подготовка приложения Перед тем. как приложение можно будет развернуть. предстоит выполнить опре­ деленные подготовительные шаrи, чтобы привести его в готовность к производствен­ ной среде. В последующих разделах будет изменен способ отображения сообщений об ошибках и настроены строки подключения для производственных баз данных. Создание контроллера и представления для отображения сообщений об ошибках В текущий момент приложение сконфигурировано на использование страющ ошибок. дружественных к разработчику, которые предоставляют полезную информацию при воз­ никновении проблемы. Конечные пользователи не должны видеть такую информацию. поэтому добавим в папку Controllers файл класса по имени с определением простого контроллера. показанного в листинге Листинг 12.12. Содержимое файла ErrorController. cs ErrorController. cs 12.12. из папки Controllers using Microsoft.AspNetCore.Mvc; namespace SportsStore.Controllers puЫic class ErrorController : Controller ViewResult Error() => View(); puЫic В контроллере определено действие Error, которое визуализирует стандартное представление. Чтобы снабдить контроллер представлением, создадим папку Error и добавим в нее файл представления кой, приведенной в листинге 12.13. Razor по имени Error. cshtml Views/ с размет­ 344 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Листинг 12.13. Содержимое файла Error.cshtml из папки Views/Error @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link rel="stylesheet" href="-/liЬ/bootstrap/dist/css/bootstrap.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-документ с сообщением о возникновении ошибки, не предоставляя никакой информации о том, что произошло. Определение настроек производственных баз данных Следующим шагом будет создание файла, который снабдит приложение строка­ ми подключения к его базам данных в производственной среде. Добавим в проект SportsStore новый файл по имени шаблона элемента ASP.NET appsettings .production. json Coпfiguratioп File тим в него содержимое, показанное в листинге Совет. В списке файлов окна Solutioп находится внутри узла Explorer appset tings. j son, с применением (Файл конфигурации ASP.NEТ) и помес­ 12.14. файл appsettings .production. j son который понадобится раскрыть, если позже вы захотите отредактировать данный файл. Листинг 12.14. Содержимое файла appsettings. production. j son из папки SportsStore "Data": { "SportStoreProducts": "ConnectionString": "Server=tcp:sportsstorecore2db.database.windows.net,1433; Initial Catalog=products;Persist Security Info=False; User ID={ваше_имя_пользователя); Password={вaш_napoль);MultipleActiveResultSets=True;Encrypt=True; TrustServerCertificate=False;Connection ) Timeout=ЗO;" ' "SportStoreidentity": { "ConnectionString": "Server=tcp:sportsstorecore2db.database.windows.net,1433; Глава 12. SportsStore: защита и развертывание 345 Initial Catalog=identity;Persist Security Info=False; User ID={ваше_имя_nопьаоватепя}; Password={вaw_naponь};MultipleActiveResultSets=True;Encrypt=True; TrustServerCertificate=False;Connection Timeout=ЗO;" Файл неудобен для чтения. т.к. строки подключений разрывать нельзя. Содержимое данного файла дублирует раздел строк подключений файла здесь используются строки подключений имени пользователя и пароля.) Кроме того, параметр был установлен в True, appset tings. j son. но Azure. (Не забудьте заменить заполнители для Mul tipleActi veResul tSets что делает возможными множество параллельных запросов и устраняет условия для возникновения распространенной ошибки, которые появляют­ ся при выполнении сложных запросов LINQ в отношении данных приложения. На заметку! При вставке в строки подключения своего имени пользователя и пароля удали­ те символы фигурных скобок, чтобы в итоге получилось Password=MyPassword, но не Password={MyPassword). Конфиrурирование приложения Теперь можно изменить код класса Startup, чтобы в случае нахождения в про­ изводственной среде приложение вело себя по-другому. Изменения представлены в листинге Листинг 12.15. 12.15. Конфигурирование приложения в файле из папки using using using using using using using using using using using using Startup. cs SportsStore System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; SportsStore.Models; Microsoft.Extensions.Configuration; Microsoft.EntityFrameworkCore; Microsoft.AspNetCore.Identity; namespace SportsStore { puЬlic class Startup { Startup(IConfiguration configuration) => Configuration = configuration; puЫic puЫic IConfiguration Configuration { get; 1 void ConfigureServices(IServiceCollection services) services.AddDbContext<ApplicationDbContext>(optio ns => options.UseSqlServer( Configuration["Data:SportStoreProducts:Connectio nString"])); puЬlic 346 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 services .AddDbContext<Appldenti t.yDbContext> ( options => options.UseSqlServer( Configuration["Data:SportStoreidentity:Connection String"JI); services.Addldentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<AppidentityDbContext>() .AddDefaultTokenProviders(); 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) { if (env.IsDevelopment()) ( app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); } else ( app.UseExceptionHandler("/Error"); app.UseStaticFiles(); app.UseSession(); app.UseAuthentication(); app.UseMvc(routes => { routes. MapRoute (name: "Error", template: "Error", defaults: new { controller = "Error", action = "Error" }) ; routes.MapRoute(name: null, template: "{category}/Page{productPage:int}", defaults: new { controller = "Product", action = "List" ) ) ; routes.MapRoute(name: null,template: "Page{productPage:int}", defaults: new { controller = "Product", action = "List", productPage = 1 ) ) ; routes.MapRoute(name: null, template: "{category}", defaults: new { controller = "Product", action = "List", productPage = 1 ) ) ; routes.MapRoute(name: null,template: "", defaults: new { controller = "Product", action = "List", productPage = 1 } ) ; routes.MapRoute(name: null, template: "{controller}/{action}/{id?}"); }) ; 11 SeedData.EnsurePopulated(app); 11 IdentitySeedData.EnsurePopulated(app); Глава 12. SportsStore: защита и развертывание Интерфейс IHostingEnvironment 347 используется для предоставления информа­ ции о среде, в которой функционирует приложение, такой как среда разработки или производственная среда. В случае производственной среды ASP.NET Core будет за­ гружать содержимое файла настройки в файле appset t ings. production. j son, чтобы переопределить appset tings. j son, а это значит, что Entity Fгamewoгk Core будет подключаться к базам данных Azuгe вместо LocalDB. Доступно множество параметров для настройки конфигурации приложения в различных средах, которые будут рас­ сматриваться в главе 14. Также были закомментированы операторы, которые производят начальное запол­ нение баз данных, что объясняется в разделе "Управление начальным заполнением баз данных" далее в главе. Применение миграций к базам данных Чтобы настроить базы данных со схемами, требующимися приложению. откроем окно командной строки или PowerShell и перейдем в папку проекта Установка среды так, что инструмент командной строки строки подключения для менения окна PowerShell Azure, dotnet SportsStore. будет использовать требует установки переменной среды. В случае при­ установка переменной среды выполняется с помощью сле­ дующей команды: $env:ASPNETCORE ENVIRONMENT="Production" Если же используется окно командной строки, то переменная среды устанавлива­ ется посредством такой команды: set ASPNETCORE ENVIRONMENT=Production Для применения миграций в проекте к базам данных занные далее команды. находясь в папке проекта Azure нужно запустить пока- SportsStore: dotnet ef database update --context ApplicationDbContext dotnet ef database update --context AppidentityDbContext Переменная среды указывает размещающую среду. которая применяется для полу­ чения строк подключения к базам данных. Если приведенные команды не работают. тогда понадобится проверить, сконфигурирован ли брандмауэр Azure для разрешения доступа к машине разработки. как было описано ранее в главе, а также корректно ли скопированы и модифицированы строки подключения. Управление начальным заполнением баз данных В листинге классе 12.15 Startup, операторы, выполняющие начальное заполнение баз данных в закомментированы. Дело в том, что команды Entity Framework Core. используемые в предыдущем разделе для применения миграций к базе данных, по­ лагаются на службы. устанавливаемые классом Startup. При наличии упомянутых операторов код начального заполнения баз данных вызывался бы перед применени­ ем миграций, приводя к ошибке и препятствуя нормальной работе миграций. Когда базы данных установлены, проблема не возникает. В случае базы данных причина в том. что метод SeedData. EnsurePopulated () products применяет миграции пе­ ред заполнением начальными данными, и в том, что начальные данные I den t i t у не добавляются в приложение до тех пор, пока к базе данных не будет применена миграция. Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 348 В отношении производственной среды мы хотим принять другой подход к запол­ нению начальными данными. Для пользовательских учетных записей мы собираемся поместить в базу данных учетную запись администратора. когда производится попыт­ ка входа. Мы добавим к инструменту администрирования средство для начального заполнения базы данных товаров, чтобы производственная система могла заполнять­ ся тестовыми данными или оставаться пустой для поступления реальных данных. На заметку! Начальное заполнение данными аутентификации в производственной системе должно делаться осторожно, а приложение обязано использовать функциональные средс­ тва, описанные в главах 28-30, для изменения пароля сразу же после того, как приложе­ ние развернуто. Начальное заполнение данными о пользователях Первый шаг в изменении способа заполнения данными о пользователях предус­ матривает упрощение кода в классе Листинг using using using using 12.16. Identi tySeedData Упрощение кода в файле (листинг 12.16). IdentitySeedData.cs из папки Models Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Identity; Microsoft.Extensions.Dependencyinjection; System.Threading.Tasks; namespace SportsStore.Models ( puЫic static class IdentitySeedData private const string adminUser = "Admin"; private const string adminPassword = "Secretl23$"; puЫic static async Task EnsurePopulated(UserМanager<IdentityUser> userМanager) { IdentityUser user = await userManager.FindByidAsync(adminUser); if (user == null) ( user = new IdentityUser("Admin"); await userManager.CreateAsync(user, adminPassword); Вместо получения EnsurePopulated () самой службы U serManage r< Iden t i tyU ser> тегрировать начальное заполнение базы данных в класс (листинг Листинг AccountController 12.17). 12.17. Начальное заполнение базы данных в файле из папки using using using using using метод получает в качестве аргумента объект. что позволяет ин­ Controllers System.Threading.Tasks; Microsoft.AspNetCore.Authorization; Microsoft.AspNetCore.Identity; Microsoft.AspNetCore.Mvc; SportsStore.Models.ViewModels; Accoun tCon troller. cs Глава 12. SportsStore: защита и развертывание using 349 SportsStore.Мodels; namespace SportsStore.Controllers [Authorize] class AccountController : Controller private UserManager<IdentityUser> userManager; private SignlnManager<IdentityUser> signlnManager; puЫic AccountController(UserManager<IdentityUser> userMgr, SigninManager<IdentityUser> signlnMgr) { userManager = userMgr; signinManager = signlnMgr; IdentitySeedData.EnsurePopulated(userМgr) .Wait(); puЫic 11 ... для краткости другие методы не показаны ... I den t i t у будет заполняться AccountController для обработки НТТР-запроса. Внесенные изменения гарантируют, что база данных каждый раз, когда создается объект Конечно, это далеко от идеала, но не существует хорошего способа начального запол­ нения базы данных, и такой подход обеспечивает возможность администрирования приложения в производственной среде и среде разработки, хотя за счет ряда допол­ нительных запросов к базе данных. Начальное заполнение данными о товарах Что касается данных о товарах, то мы собираемся снабдить администратора кноп­ кой, щелчок на которой приведет к начальному заполнению базы данных, когда она пуста. Первым делом изменим код начального заполнения, чтобы задействовать в нем интерфейс, который позволит обращаться к службам, предоставляемым конт­ роллером, а не классом Startup (листинг 12.18). Кроме того, мы закомментировали операторы для автоматического применения любых ожидающих миграций, которые могут привести к утере данных и должны использоваться с особой осторожностью в производственных системах. Листинг 12.18. Подготовка к ручному начальному заполнению в файле из папки using using using using using SeedDa ta. cs Models System.Linq; Microsoft.AspNetCore.Builder; Microsoft.Extensions.Dependencyinjection; Microsoft.EntityFrameworkCore; Systeш; namespace SportsStore.Models { puЫic static class SeedData static void EnsurePopulated(IServiceProvider services) ApplicationDbContext context = services.GetRequiredService<ApplicationDbContext> (); puЬlic 11 context.DataЬase.Мigrate(); if (!context.Products.Any()) { context.Products.AddRange( Часть 1. Введение в инфраструктуру ASP NET Core MVC 2 350 // ... для краткости другие операторы не показаны ... ) ; context.SaveChanges(); Следующий шаг заключается в обновлении контроллера Admin с целью добавле­ ния метода действия, который инициирует операцию начального заполнения (лис­ тинг 12.19). Листинг 12.19. Начальное заполнение базы данных в файле из папки using using using using AdminController. сз Controllers Microsoft.AspNetCore.Mvc; SportsStore.Models; System.Linq; Microsoft.AspNetCore.Authorization; namespace SportsStore.Controllers { [Authorize) class AdminController : Controller private IProductRepository repository; puЫic AdminController(IProductRepository repo) { repository = repo; puЫic puЫic ViewResult Index() => View(repository.Products); // ... для краткости другие методы не показаны ... [HttpPost] IActionResul t SeedDa taЬase () { SeedData.EnsurePopulated(HttpContext.RequestServices); return RedirectToAction(nameof(Index)); puЬlic Новое действие декорируется атрибутом лью запросов POST, HttpPost, так что оно может быть це­ и после начального заполнения базы данных перенаправляет браузер на метод действия Index (). Осталось лишь создать кнопку для начально­ го заполнения базы данных, которая будет отображаться, когда база данных пуста (листинг Листинг 12.20). 12.20. Добавление кнопки для начального заполнения базы данных в файле @model Index. cshtml IEnumeraЫe<Product> @{ ViewBag.Title = "All Products"; Layout = "_AdminLayout"; из папки Views/Admin Глава 12. SportsStore: защита и развертывание @if (Model.Count() ==О) { <div class="text-center m-2"> <form asp-action="SeedDataЬase" method="post"> <button type="suЬmit" class="Ьtn Ьtn-danger">Seed </form> </div> else { <tаЫе class="taЫe 351 DataЬase</button> taЫe-striped taЫe-bordered taЬle-sm"> <tr> <th class="text-right">ID</th > <th>Name</th> <th class="text-right">Price< /th> <th class="text-center">Actio ns</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.ToString("c")</td> <td class="text-center"> <form asp-action="Delete" method="post"> <а asp-action="Edit" class="Ьtn Ьtn-sm Ьtn-warning" asp-route-productid="@ite m.ProductID"> Edit </а> <input type="hidden" name="ProductID" value="@item.ProductID" /> <button type="submit" class="btn btn-danger btn-sm"> Delete </button> </form> </td> </tr> </tаЫе> <div class="text-center"> <а asp-action="Create" class="Ьtn </div> Ьtn-primary">Add Product</a> Развертывание приложения Чтобы развернуть приложение, щелкнем правой кнопкой мыши на элементе про­ екта SportsStore в окне Solutioп Explorer (проекта, но не решения) и выберем в кон­ текстном меню пункт PuЫish (Опубликовать). Среда метод опубликования (рис. Visual Studio преД1Iожит выбрать 12.2). Что делать, если развертывание терпит неудачу? Самой главной причиной неудачи развертывания являются строки подключения, либо пото­ му, что они были некорректно скопированы из среды Azure, либо из-за того, что при редак­ тировании для вставки имени пользователя и пароля была допущена ошибка. 352 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 В случае отказа развертывания начинать поиск проблемы нужно со строк подключения. Если команды dotnet ef database update из раздела "Применение миграций к базам данных" выше в главе не приводят к ожидаемым результатам, то развертывание потерпит неудачу. Если команды отработали успешно, но развертывание закончилось неудачей, тог­ да необходимо проверить, установлена ли переменная среды, поскольку есть вероятность того, что вместо базы данных в облаке была подготовлена локальная база данных. PuЬlish Connedtd ~ces PuЬ11sh your 4рр to Azure or 1nothtr host. lt1rn more Microюft Azure llS, FТР, <tc AppS.Мct foldtr PuЫnh S.ltct Ex1stong Рис. Выберем вариант 12.2. > Выбор метода опубликования Microsoft Azure Арр Service (Служба приложений Microsoft Azure) Create New (Создать новую); переключатель и удостоверимся в выборе переключателя Select Existiпg (Выбрать существующую) используется для обновления существующего развернутого приложения. Будет предложено предоставить детали для развертыва­ ния. Начнем со щелчка на кнопке учетных данных Add ап Account (Добавить учетную запись) и ввода Azure. После ввода учетных данных можно выбрать имя для развернутого приложения и ввести информацию относительно службы. которая будет зависеть от типа имею­ щейся учетной записи Azure, службы развертывания (рис. желаемой области для развертывания и требующейся 12.3). После того , как служба сконфигурирована, щелкнем на кнопке Create (Создать). Как только служба установится. будет выдана сводка по операции опубликования. ко­ торая отправит приложение в службу размещения (рис. 12.4). Щелкнем на кнопке PuЫish (Опубликовать), чтобы запустить процесс разверты­ вания. Выбрав пункт ViewqQther Windows Web PuЫish Activity (Активность веб-развертывания) в меню (Видq Другие окна) среды Visual Studio. можно наблюдать за хо­ дом развертывания. Наберитесь терпения. т. к. отправка всех файлов из проекта служ­ бе Azure может занять некоторое время. Последующие обновления будут проходить быстрее, потому что передаче подлежат только измененные файлы. По завершении развертывания среда Visual Studio откроет новое окно браузера для развернутого приложения. Поскольку база данных товаров пуста, отобразится компоновка, показанная на рис. 12.5. Глава 12. SportsStore: 353 защита и развертывание х Create Арр Service r1 ···Microsoft account Host your web and moblle applications. REST APls, and more in Azure Нosting Srrvkes Chang• Тур•• AppNtmt SportsStor.Ccrt-2 Subюiption Visual Studio Enterprise: Resourct Group Арр S.Мсе v 1 Ntw". v 1 New... Plon SportsStor.Cort2Plon' Clklcing the Crt•te Ьutton will cre•I• the following Azure resources Explore 4dd1t1on.al Azurt seJV1te-s Арр S.rvict • SportsStor.Cort-2 Арр S.rvict Plon · SportsStor.Cort2Pl•n lf you h.wt rtmovod your sptnding lim~ or you or• u51ng Р•у •• You Go, th"e may Ь• mon.t•ry imp•ct if you provision •ddition1I rtsourcts. Le&m Mcrt Export..• Crute Рис. 12.З. Создание новой службы приложений l J C•ncel Azure ~ SportsStore ~ SportsStort- - PuЬlish Ccnn«ted Se.nticн J>uЫ1sh your •РР to .A.."Vrt or ьnother ho-st. lf.tm mcrt Э!] SportsS!ortCort-2 · Vltb Dtploy • ' PuЫ1th Summary QJ Stlt URL http: spcrtsstof~tore·2.41.u1twt'!нrt.es..net R.tюur<t Group cort.2 Prl'1r1ew. Confi9ur11t1on Reltast Ptname profi t~ Userrчme SSport•St0<.Cort-2 Ot:fett prof1le Pass't'tord Рис. 12.4. Сводка по операции опубликования D Х 354 Часть 1. Введение в инфраструктуру ASP NET Core MVC 2 Home _J 1 Рис. Перейдем на вателя 12.5. URL вида Начальное состояние развернутого приложения /Admin/Index и для аутентификации введем имя пользо­ Admin и пароль Secret123$. Произойдет начальное заполнение базы данных Identity по требованию. что даст возможность войти в административную часть приложения (рис . 12.6). D All Products Х Af 1Products ID Name Price 1 Kayak $275.00 2 lifejacke S48.95 З Soccer Ball $19.50 4 Corner Flags SЗ4.95 Actions 1111 1111 Е11 " "" " $79,500.00 DIEll Thinking Сар $16.00 111111 7 Unsteady Chair $29.95 8 Human Chess Воагd 575.00 5 Stadium б $1 ,200.00 9 Bling-BlingXing Mfi§§E Рис. 12.6. Экран администрирования Глава 12. SportsStore: защита и развертывание Щелкнем на кнопке Seed Database (Заполнить базу данных). чтобы запол нить базу данных товаров, что приведет к результату. представленному на рис. можно перейти на корневой С URL прил ожения 355 12.7. Затем и работать с ним обычным образом. <D sportsstorecore-2.azurewebs1tes.net1adm1n/naex All Products Seed Database Add Product Рис. 12. 7. Заполненная база данных Резюме В этой и предшествующих главах демонстрировалось применение инфраструкту­ ры ASP.NET Core МVС для создания реалистичного приложения электронной коммер­ ции. В приведенном расширенном примере были проиллюстрированы многие основ ­ ные средства MVC: контролле ры . методы действий. маршрутизация , представления , метаданные, проверка достоверности , компоновки, аутентификация и т. д. Вы такж е видели, как пользоваться рядом ключевых технологий, имеющих отношение к том числе Eпtity Framework Core, MVC, в внедрение зависимостей и модульное тестирование. Результатом стало приложение с ясной и ориентированной на компоненты архитекту­ рой, обеспечивающей разделени е обязанностей, и кодовой базой, которую будет лег ­ ко ра с ширять и сопровождать . Итак. разработка приложения SportsStore завершена . В сл едующе й главе будет показано. как применять приложений ASP.NET Core MVC. Visual Studio Code для со здания ГЛАВА 13 Работа с Visual Studio Code будет показано, как создать приложение ASP.NET Соге MVC с в настоящей главе Vlsual Studlo Code применением межплатформенного редактора с открытым кодом производства Microsoft. Несмотря на название. продукт Visual Studio Code не имеет отношения к Visual Studio и основан на инфраструктуре Electron, используе­ мой редактором Atom. который популярен среди разработчиков, применяющих дру­ гие инфраструктуры для построения неб-приложений, такие как Angular. Продукт Visual Studio Code поддерживает операционные системы Windows. macOS и наиболее популярные дистрибутивы Linux. Он сформировался в удобную и полно­ функциональную среду разработки, хотя и лишен всех украшений, предлагаемых Visua\ Studio. Я обнаружил, что использую среду Vlsua\ Studlo Code все чаще и чаще, поскольку с ней просто работать, она быстрая и располагает хорошей поддержкой других языков вроде JavaScript и ТypeScript. Настройка среды разработки Процесс настройки Visual Studio Code требует выполнения определенной работы. т.к. некоторая функциональность. включенная в состав Code Vlsua\ Studio, в Visual Studlo обрабатывается внешними инструментами. Одни инструменты идентичны тем. которые среда Visua\ Studio применяет "за кулисами'', но другие являются новыми в .NET и могут быть незнакомыми. Хорошая новость в том, что мире разработки для эти инструменты широко используются разработчиками, которые ориентируются на другие инфраструктуры для построения неб-приложений, причем качество и функ­ циональные средства находятся на высоком уровне. В последующих разделах мы ис­ следуем процесс установки Visual Studio Code наряду с важными инструментами и дополнениями, требующимися для разработки приложений Установка MVC. Node.js В мире разработки клиентской стороны Node.js (или просто Node) представляет собой исполняющую среду. на которую полагаются многие популярные инструменты разработки. Продукт Node был создан в 2009 году как простая и эффективная испол­ няющая среда для серверных приложений. написанных на языке нована на механизме JavaScript, интерфейс для выполнения кода Исполняющая среда Node.js применяемом в браузере JavaScript JavaScrlpt. Она ос­ Chrome, и предлагает АРl­ за пределами среды браузера. достигла определенных успехов в качестве сервера приложений, но в данной главе она интересна тем, что предоставляет основу для нового поколения межплатформенных инструментов построения и диспетчеров па­ кетов. Ряд интеллектуальных проектных решений, внедренных командой разработ- Глава 13. Работа с Visual Studio Code 357 Node. и межплатформенная поддержка. обеспечиваемая исполняющей средой JavaScript браузера Chrome. создали благоприятную возможность. которой восполь­ чикав зовались полные энтузиазма проектировщики инструментов. особенно те из них. кто желал поддерживать разработку веб - приложений . На заметку! Доступны две версии Node.js. Версия (Loпg LTS Term Support - долгосрочная поддержка) предоставляет стабильный фундамент для развертывания в производствен­ ных средах, где изменения должны быть сведены к минимуму. Обновления версии LTS выпускаются каждые 6 месяцев и сопровождаются в течение 18 месяцев . Версия Curreпt (текущая) является более часто изменяющимся выпуском , в котором преимущество от ­ дается новым средствам взамен стабильности. В настоящей главе мы будем применять выпуск Curreпt . Установка Node.js в Windows Загрузим и запустим установщик nodej s. o rg . При установке Node.js Node.js для Windows из веб-сайта h t tp : / / Add to Windows, понадобится обеспечить выбор варианта РАТН (Добавить в переменную Р А Т Н). На рис . 13. l показан установщик для который предлагает модифицировать переменную среды РАТН в качестве варианта установки. ~~t the way you v.·ant feaЬ.Xes to Ье lnstaeed. Instal the core Node.js runtirne (~.ехе). 11"5 fe11ture reQIJlfes lбМВ on your hard drJve. !t has 2 of 2 suЬfeaЬ.Xes selected. ТМ suЬfeaь.xes r~e 2ОКВ on YfXX > < harddn\le. Next Reset Рис . Установка 13.1. Node.js Установщик для в Добавление пути к Node.js в переменную среды РАТН macOS macOS можно загрузить из веб - сайта htt ps : / /nodej s . o r g . Запустим установщик и примем стандартные настройки. Когда устано вка заве ршит­ ся. удо стоверимся в наличии пути / us r / l о са l / Ьi n внутри переменной $ РАТ Н. 358 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Установка Node.js в Linux Самый простой способ установки Node.js в Liпux предполагает использование дис­ Node предоставила инструкции для основных петчера пакетов: команда разработчиков дистрибутивов по адресу https: / /nodej s. org/en/download/package-manager /. В среде Ubuпtu для загрузки и установки ные в листинге Листинг 13.1. Node.js применяются команды, приведен­ 13.1. Установка Node.js sudo curl -sL https://deb.nodesource.com/setup sudo -Е bash -sudo apt-get install -у nodejs Проверка установки 6.х Node После завершения установки откроем новое окно командной строки и запустим команду. показанную в листинге 13.2, чтобы проверить. работает ли Node, и отобра­ зить номер установленной версии. Листинг 13.2. Проверка успешности установки Node node -v Если установка прошла успешно, и путь к Node добавился в переменную среды РАТН, то появится номер версии. На момент написания главы текущей версией была 6.11.2. Node В случае если во время проработки примеров. рассмотренных в главе. вы получаете неожиданные результаты, тогда попробуйте воспользоваться указанной конкретной версией. Установка Продукт Git Visual Studio Code включает интегрированную поддержку ся отдельная установка для поддержки инструмента Bower. Git, но требует­ который применяется при управлении пакетами клиентской стороны. Установка Git в Windows Загрузим и или macOS запустим установщик из веб-сайта ht.tps: //git-scm. сот/ downloads. Установка Git в Linux В большинстве дистрибутивов Liпux инструмент Git уже установлен. При желании установить его в любом случае следует ознакомиться с инструкциями по установке для своего дистрибутива, которые доступны по адресу: https://gH-scm.com/download/linux В среде Ubuпtu используется команда, представленная в листинге Листинг 13.3. Установка Git sudo apt-get install git в Ubuntu 13.3. Глава 13. Работа с Visual Studio Code Проверка установки 359 Git После завершения установки запустим в новом окне командной строки/терминала команду, приведенную в листинге инструмент Листинг чтобы проверить, установлен и доступен ли 13.4, Git: Проверка успешности установки 13.4. Git git --version Git. На 2.14.1. а Команда выведет версию установленного пакета последней версией Установка Windows и macOS была момент написания главы для Linux - 2. 7.4. Bower Node.js Продукт NPM), для Git Node (Node Package Manager - поступает с диспетчером пакетов который применяется для загрузки и установки пакетов разработки, напи­ санных на Для целей текущей главы нужен единственный пакет JavaScript. Bower, который используется для управления пакетами клиентской стороны и был описан в главе 6. Запустим команду. показанную в листинге вить Воwег в Листинг 13.5, чтобы загрузить и устано­ Windows. 13.5. Установка пакета Bower в Windows npm install -g [email protected] Linux и macOS применяется (листинг 13.6). В средах мандой Листинг та же самая команда. но с обязательной ко­ sudo 13.6. Установка пакета Bower в Linux и macOS sudo npm install -g [email protected] Установка .NET Core При разработке приложений Core. ASP.NET Core MVC требуется исполняющая среда .NET Для каждой поддерживаемой платформы предусмотрен собственный процесс установки. описание которого доступно по адресу В Microsoft предлагают установщики для инструкции для установки в Установка . NET Core Чтобы установить в Linux Windows Windows www.microsoft.com/net/core. и macOS. с использованием архивов и а также предоставляют tar. macOS .NET Core в Windows .NET Core SDK. или в macOS. нужно просто загрузить и запустить установщик Установка .NET Core По адресу в Linux www.microsoft.com/net / core предлагаются инструкции по установ­ .NET Core в большинстве популярных дистрибутивов Linux. В главе применяется Ubuntu, и процесс требует первоначальной настройки новой подачи для apt-get с использованием команд из листинга 13. 7. ке 360 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Листинг 1З.7. Подготовка к установке .NET Core в Ubuntu Linux sudo sh -с 'echo "deb [arch=arnd64] https://apt-rno.trafficrnanager.net/repos/ dotnet-release/xenial rnain" > /etc/apt/sources.list.d/dotnetdev.list' sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:BO --recv-keys 417А0893 sudo apt-get update Далее производится установка Листинг .NET Core (листинг 13.8). 13.8. Установка .NЕТ Core в Ubuntu Linux sudo apt-get install dotnet-sdk-2.0.0 Проверка установки .NET Core Независимо от имеющейся платформы проверка того, что инфраструктура .NЕТ Соге установлена и готова к использованию, производится одинаково. Откроем новое окно командной строки/терминала и выполним команду, приведенную в листинге Листинг 13.9. 13.9. Проверка версии .NЕТ Core dotnet --version Команда dotnet запускает исполняющую среду мер версии установленного пакета выпуском был 2.0.0, .NET Core. .NET, после чего отобразится но­ На момент написания главы текущим но ко времени выхода книги он с высокой вероятностью будет заменен более новым выпуском. Установка Visual Studio Code Visual Studio Code, https: / / code. visualstudio. сот. Установочные Windows. macOS и популярных дистрибутивов Linux. Самым важным шагом является загрузка и установка редактора который доступен на веб-сайте пакеты предусмотрены для Понадобится загрузить и установить пакеты для выбранной платформы. На заметку! Компания Microsoft предлагает новые выпуски Visual Studio Code ежемесячно, а это значит, что устанавливаемая вами версия будет отличаться от версии, которая приме­ няется в главе. Хотя основы должны остаться теми же, выполнение некоторых примеров может потребовать определенной доли экспериментирования. Установка Visual Studio Code Чтобы установить в Windows Visual Studio Code для установщик. После завершения процесса окно редактора (рис. Установка Windows, необходимо просто запустить Visual Studio Code запустится и появится 13.2). Visual Studio Code Продукт в macOS VisuaJ Studio Code предоставляется в виде архива zip для Мае, который до­ https: / /go .microsoft. сот/ fwlink/?LinkID=620882. Раскроем архив и дважды щелкнем на содержащемся в нем файле Visual Studio Code. арр. чтобы запустить Visual Studio Code и получить окно редактора (см. рис. 13.2). ступен для загрузки по адресу Глава 13. Работа с Visual Studio Code Установка Visual Studio Code в 361 Linux Компания Microsoft предлагает файл . deb для Deblan и Ubuntu и файл . rpm для Red Hat. Fedora и CentOS. Необходимо загрузить и установить файл для предпочита­ емого дистрибутива Linux. Поскольку в главе используется Ubuntu. нужно загрузить файл . deb и установить его с помощью центра приложений Ubuntu. После завершения установки выполним команду, приведенную в листинге 13. l О. что ­ бы запустить Visual Studio Code и получить окно редактора, показанное на рис. 13.2. Листинг 13.10. Запуск Yisual Studio Code в Linux / u s r / share/code / code Проверка установки Visual Studio Code Тестирование успешности установки Visual Studio Code сводится к проверке воз ­ можности запуска приложения и открытию окна редактора (см. рис . 13.2). (Цветовая схема была изменена. т.к. стандартные темные цвета не очень хорошо подходят для получения экранных снимков.) " ·• /Jr.trt\td-1 · V~wJSilllfto.Codt - rn llrФtltd•1 ' Рис. 13.2. Запуск Visual Studio Code Visual Studio Code х 1 Установка расширения С# для Редактор Х (J в средах Windows, macOS и Ubuntu Linux Visual Studio Code подде рживает функциональность. специфичную для языка. через расширения, хотя это не те же самые расширения, которые поддержи­ ваются средой Visual Studio. Самое важное расширение. касающееся разработки при­ Core MVC, добавляет поддержку для языка С#, отсутствие которой ложений ASP.NEТ может выглядеть как странное упущение в базовой установке . Тем не менее. такое положе ние дел отражает тот факт, что в Microsoft позиционировали Vis ual Studio Code 362 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 как универсальный межплатформенный редактор. который охватывает широчайший спектр языков и инфраструктур. Чтобы установить расширение С#. щелкнем на значке расположенном в левой части окна и отыщем в списке расширение С# как показано на рис. Visual Studio Code. В for Visual Studio Code (Расширения) , Extensions поле поиска введем (С# для csharp Visual Studio Code). 13.3. [D С# Ф Ш•~ Microsoft С# Рис. Щелкнем на кнопке lnstall (Установить) и Reload OrnniSharp) Visual Studio Code загрузит и установит (Перезагрузить), чтобы перезапустить и активизировать расширение (рис. С# Microsoft 1 t;> 331463 СИ Ьу 36 1 З.З. Нахождение расширениR С# расширение. Щелкнем на кнопке Studio Code for Visual Studio Code (powered ***; ". 1 Visual 13.4). ***; L1cense 36 for V1sual Stud10 Code (pcw1ered Ьу OmnlSмrp). f !m Рис. Создание проекта ~rnnst•U 13.4. Включение расширениR С# ASP.NET Core Редактор проектов строки Visual Studio Code не имеет интегрированной поддержки для создания ASP.NET Core. но создавать новые проекты можно с применением командной d o tnet . Для корректной настройки проекта важно создать правильную структуру папок , особенно в случае, когда проект сопровождается модульными тестами. Создадим про ­ ект под названием InvitesProjects рая будет содержать проект в подходящем местоположении ASP.NET Core MVC Далее создадим внутри папки - папке. кото­ и проект модульного тестирования. Invit esProjects папку по имени Par t y i nvite s, с использованием окна командной строки перейдем в эту папку и запустим команду. приведенную в листинге 13.11. Глава 13. Работа с Visual Studio Code Листинг 1З.11. Создание нового проекта в папке d ot net new web --language Команда d o tnet new С# 363 Partyinvi tes --f ramework net co reapp 2 .0 предоставляет доступ посредством командной строки к 13. 1 1 шаблон web соответствует шаб­ Empty (Пустой) из Visual Studio. который применялся в предшествующих главах. В табл. 13. 1 описан набор шаблонов проектов, предназначенных для разработки при­ ложений ASP.NET Саге. (Существуют и другие шаблоны , но при построении приложе­ ний ASP.NET Саге они не используются. Просмотреть полный список можно посредс­ твом команды d o tnet new --help .) шаблонам проектов . и указанный в листинге лону Таблица 13.1. dotnet new для Шаблоны разработки приложений ASP.NEТ Core Описание Имя web Empty, применяемый в предшествующих главах, который созда­ ASP.NET Core, но не включает поддержку инфраструктуры MVC Это шаблон ет проект mvc Это шаблон Web Applicatioп (Model-View-Coпtroller) (Веб-приложение 2, который со­ MVC, а также запол­ (модель-представление-контроллер)), используемый в главе здает проект, включающий поддержку инфраструктуры нители для контроллеров и представлений xuni t Это шаблон xUпit (.NET Core)), Test Project (.NET Core) (Проект тестирования xUпit который настраивает модульное тестирование с помощью пакета xUпit, как объяснялось в главе 7 Подготовка проекта с помощью Visual Studio Code Visual Studio Code. выберем пункт Ореп Folder (Открыть InvitesProj ects и щелкнем на кнопке папку). Среда Visual St.udio Code откроет проект и автомати­ Чтобы открыть проект в папку) в меню Select Folder File (Файл), перейдем в папку (Выбрать чески установит набор пакетов. требующихся для редактирования и отладки прило­ жений С#. Спустя несколько секунд после начала редактирования файла появится сооб щение. предлагающее добавить элем енты в проект (рис . Рис. Щелкнем на кнопке 13.5. Yes 13.5). Приглашение добавить активы в проект (Да). Среда Visual Studio Code создаст папку . v s code и добавит в нее файлы, которые конфигурируют процесс построения. По умолчанию Visual Studio Code прим еняет компоновку из трех разделов. Боковая п анель. выделен ­ ная на рис. 13.6, обеспечивает доступ к главным областям функциональности . Самая верхняя кнопка позволяет открыть панель проводника. которая отобразит содержимое ранее открытой папки . Остальные кнопки предоставляют доступ к средс­ тву поиска. к интегрированному управлению исходным кодом. к отладчику и к набору установленных расширений. 364 Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 OPEN EDIТORS IN\IJTESPROJECТS .с .vscode Partylnv1tes ~ btn • Obj V\ '\.Yroot ~ Partylnv1tes.csproJ С• С Рис. Pr09ram.cs Start p.cs 13.6. Боковая панель Visual Studio Code Щелчок на имени файла в панели проводника приводит к открытию файла для редактирования. Допускается редактировать несколько файлов одновременно. Можно создавать новые панели редактора. щелкая на кнопке дактора) в правой верхней части окна . Редактор обладая неплохой поддержкой пакетов NuGet и lntelliSense и Split Editor (Разделить окно ре­ Visual Studio Code довольно хорош, содействием в завершении имен и версий Bower. Кроме содержимого папки проекта панель проводника показывает. какие файлы в текущий момент редактируются. Это позволяет легко сосредоточиться на подмножес­ тве файлов, с которыми производится работа, что является удобным дополнением. когда приходится иметь дело с поднабором связанных файлов в крупном проекте. Управление пакетами клиентской стороны Как и в Visual Studio. при управлении пакетами Visual Studio Code используется инструмент Bower. клиентской стороны в проектах хотя требуется дополнительная работа . Первым делом нужно добавить файл по имени для того, чтобы указать мыши на папке Bower. Partylnvi tes . bowe rr c . который применяется где устанавливать пакеты. Щелкнем правой кнопкой и выберем в контекстном меню пункт New File (Новый файл), как демонстрируется на рис. Установим имя файла в букв r) 13.7. . bowerrc (обратите внимание на наличие в имени двух и поместим в него содержимое. приведенное в листинге 13. 12. Глава 13. Работа с Visual Studio Code 365 • Ol'EN EOITORS INV!ТESPROJfCТS • • vscode • Partylnv1 1>< • bin №w f1I• ~ • ObJ №w • wwwroc R<v••I n Explortr • 1f • " Optn 1n Comm•nd Prompt .с ! Partyln С• Progran С• St. up. Foldtr F1nd 1n Foldtr Сору •V СоруР• h Rl!'namt D Рис. 1З.7. Создание нового файла Листинг 13.12. Содержимое файла . bowerrc "d irectory ": "wwwroot/lib " Затем создадим файл по имени листинге Листинг bower . j s on с содержимым. представл енным в 13. 13. 13.13. Содержимое файла bower. j son "name ": "Partyi n vites ", "pri va te": true, " dep ende ncies": { "boo tstrap" : "4.0.0-alpha. 6 " Во с пользуемся окном ком андной строки/терминала, чтобы выполнить в папке Partylnvi tes команд.у из листинга 13. 14. Листинг 1З.14. b o wer i n s tall Bower для bowe r. j son. которая применяет инструмент загрузки и установки пакетов клиентской стороны . указанных в файле Установка пакетов клиентской стороны 366 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Конфигурирование приложения Процесс инициализации проекта создал пустой проект без поддержки тинге 13.15 приведены изменения, подлежащие внесению в файл бы настроить MVC 13.15. что­ 14. Добавление поддержки из папки using using using using using using using using В лис­ с наиболее базовой конфигурацией, используя операторы, которые были описаны в главе Листинг MVC. Startup. cs, MVC в файле s tartup. cs Partyinvi tes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace Partyinvites { puЫic class Startup { void ConfigureServices(IServiceCollection services) { puЫic services.AddМvc(); puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseМvcWithDefaultRoute(); Построение и запуск проекта Чтобы построить и запустить проект, понадобится выполнить в окне командной строки/терминала команду из листинга Листинг 13.16. 13.16, находясь в папке Partylnvi tes. Запуск приложения dotnet run Среда Visual Studio Code скомпилирует код в проекте и с помощью сервера при­ Kestrel, рассматриваемого в главе 14, запустит приложение, ожидая НТГР­ запросы на порте 5000. Поскольку в Visual Studio Code не предоставляется поддержка для обнаружения ложений изменений в файлах классов С#, после их модификации потребуется остановить при­ ложение и запустить его снова. Чтобы протестировать приложение, необходимо открыть окно браузера и перейти на URLвида Ошибка htt.p: / /localhost: 5000. 404 Будет получен ответ, представленный на рис. 13.8. связана с тем, что в текущий момент в проекте отсутствуют какие-либо контроллеры для обработки запросов. Глава 13. Работа с Visual Stud io Code 367 х С , Ф localhost.5000 Code: 404; Not Found Stэtus Рис. 13.8. Тестирование примера приложения Воссоздание приложения Partyinvi tes Все подготовительные шаги завершены, т.е. мы можем сосредоточиться на созда­ нии приложения из главы 2. MVC. Мы собираемся воссоздать простое приложение Partyinvites но с несколькими изменениями и дополнениями. которые подчеркнут осо­ бенности работы с Visual Studio Code. Создание модели и хранилища Для начала щелкнем правой кнопкой мыши на папке рем в контекстном меню пункт Установим имя папки в New Folder Partyin vites Mode ls . .. Filo Edtt S•lec on V10" Go DoЬug •sks H•lp EXPLORER • OPfN EDIТORS using Systom; using Syst~.Coll•ctions.Goм using Sy:>t~m. Linq; using Systom. Throoding. Tosks; usi ng ~licrosoft. AspllotCoro. Bui usiлg Microsoft .AspNotCoro.Ho .i •LI а (j eJ • .vscodt! • Partytrivi • b1n • оЬj • WV.'WflX Ь .bowerr• ~ bo.-er.J! !Ь Partyll'l\I С• Progran №wF1lo асе NowFoldor Ыiс ~ Rtv•ol tn Explor~ Partyinvi tes { class Startup { ft R. !+ Opon 11'1 Command Prompt ft+ рuЫ ic void Configur•St servic•s .Addl·lvc() · F1nd 1n Fold•r puЫic Сору С• Startup. с Сору Ро h А t• • ft• void Configure арр. Us •D•v• loperf арр. UseStotusCode арр. UseStoticFil app .UseNvctHthDef Reni1me Dt~t• Рис. 13.9. и выбе­ (Новая папка). как показано на рис . Создание новой папки 13.9. 368 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 Щелкнем правой кнопкой мыши на папке в контекстном меню пункт New File, поместим в файл код С# из листинга Visual Studio Code в панели проводника, выберем GuestResponse. cs и 13.17. Работа с редактором Продукт Models установим имя файла в Visual Studio Code (и расширение С#, установленное ранее в главе) предлагает полнофункциональные средства для редактирования файлов С#, а также файлов распро­ страненных веб-форматов, подобных JavaScript, CSS и HTML. Во многих отношениях на­ MVC в Visual Studio Code имеет массу общего с тем же процессом в Visual Studio: доступна поддержка lпtelliSense, кодирование цветом и подсветка писание приложения редакторе ошибок (с советами по их исправлению). Основной недочетом Visual Studio Code является отсутствие возможности настройки, осо­ бенно когда речь идет о форматировании кода. На момент написания главы были доступны варианты конфигурации для других языков, но расширение С# не допускает настройки, что несколько затрудняет работу, если предпочитаемый вами стиль написания кода не совпада­ ет со стилем, предлагаемым по умолчанию. Однако в целом редактор отличается быстрой реакцией и легкостью в применении, так что написание приложений или Linux Листинг MVC в среде macOS не выглядит второсортным процессом. 13.17. Содержимое файла GuestResponse. cs из папки Partyinvi tes/Мodels using System.ComponentModel.DataAnnotations; namespace Partyinvites.Models class GuestResponse { int id {get; set; ) [Required(ErrorMessage = "Please enter your name")] puЫic puЫic 11 puЫic Пожалуйста, введите свое имя string Name { get; set; ) [Required(ErrorMessage 11 = "Please enter your email address")] Пожалуйста, введите свой адрес электронной почты [RegularExpression(".+\\@.+\\ .. +", ErrorMessage = "Please eпter а valid email address")] 11 puЬlic Пожалуйста, [Required(ErrorMessage = 11 puЫic "Please enter your phone number")] Пожалуйста, введите свой номер телефона string Phone { get; set; ) [Required(ErrorMessage = 11 puЬlic введите допустимый адрес электронной почты string Email { get; set; ) "Please specify whether you'll attend")] Пожалуйста, укажите, примете ли участие bool? WillAttend ( get; set; ) Добавим в папку Models файл по имени IReposi tory. cs с определением интер­ фейса, приведенным в листинге щей главе от приложения из 13.18. Самое важное отличие приложения в настоя­ главы 2 связано с тем, что мы планируем хранить данные модели в постоянной базе данных. Интерфейс IRepository описывает, каким обра­ зом приложение будет получать доступ к данным модели. не указывая реализацию. Глава 13. Работа с Visual Studio Code Листинг 13.18. Содержимое файла IRepository.cs из папки 369 Partyinvites/Models using System.Collections.Gener ic; namespace Partyinvites.Models { puЫic interface IRepository { Responses {get; IEnumeraЫe<GuestResponse> void AddResponse(GuestResponse response); Добавим в папку Models файл по имени ApplicationDbContext. cs в нем класс контекста базы данных (листинг Листинг и определим 13. 19). 13.19. Содержимое файла ApplicationDbContext.cs из папки Partyinvi tes/Models using Microsoft.EntityFramework Core; namespace Partyinvites.Models { puЫic DbContext { class ApplicationDbContext puЫic ApplicationDbContext() {} protected override void OnConfiguring(DbContextOp tionsBuilder builder) { builder.UseSqlite("Filena me=./Partyinvites.db"); puЫic DbSet<GuestResponse> Invites [get; set;} Средство SQLite хранит данные в файле, который указывается классом контекс­ та. В создаваемом примере приложения данные будут храниться в файле по имени Partyinvites. db, что определено в методе OnConf iguring (). Чтобы завершить набор классов, необходимых для хранения и доступа к данным модели, требуется реализация интерфейса контекста базы данных. Добавим в папку IReposi tory, Models файл по и поместим в него код. представленный в листинге Листинг которая использует класс имени EFReposi tory. cs 13.20. 13.20. Содержимое файла EFRepository.cs из папки Partyinvites/Models using System.Collections.Gener ic; namespace Partyinvites.Models { class EFRepository : IRepository private ApplicationDbContext context = new ApplicationDbContext(); puЫic puЫic IEnumeraЫe<GuestResponse> Responses => context.Invites; void AddResponse(GuestResponse response) { context.Invites.Add(respo nse); context.SaveChanges(); puЫic Часть 1. Введение в инфраструктуру ASP.NET Саге MVC 2 370 Класс главе 8 EFReposi tory следует шаблону, который похож на тот, что применялся в для настройки базы данных класса ConfigureServices () рый сообщает инфраструктуре класса EFReposi tory, SportsStore. Startup В листинге 13.21 видно, что к методу добавлен оператор конфигурирования. кото­ ASP.NET Core о необходимости создания экземпляра когда требуются реализации интерфейса IReposi tory. мощью средства внедрения зависимостей (рассматриваемого в главе Листинг 13.21. Конфигурирование хранилища в файле из папки using using using using using using using using using с по­ 18). Startup. cs Party!nvi tes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Partyinvites.Models; namespace Partyinvites { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) services.AddTransient<IRepository, EFRepository>(); services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Создание базы данных В оставшихся главах книги всякий раз, когда нужно продемонстрировать функци­ ональность, требующую постоянства данных. используется средство представляет собой упрощенную версию ступно только в среде Windows, Microsoft SQL Server. LocalDB. которое LocalDB до­ ASP.NET Саге MVC Но средство поэтому при создании приложений на других платформах понадобится какая-то альтернатива. Наилучшей альтернати­ вой LocalDB является SQLite - не нуждающаяся в конфигурировании межплатфор­ менная система управления базами данных. Она может встраиваться в приложения. и для нее компания Microsoft включила поддержку в Entity Framework Core. которая и представляет собой уровень доступа к данным, обычно применяемый в приложениях ASP.NET Core МVС. В последующих разделах мы рассмотрим процесс добавления базы данных SQLite в проект и ее использование в качестве хранилища данных для ответов на приглашения поучаствовать в вечеринке. Глава 13. Работа с Visual Studio Code Использование Одна из причин того, что LocalDB SQLite при разработке является настолько полезным инструментом, связана с возможностью разработки с применением механизма баз данных лает переход в производственную среду рисков. 371 SQL Server SQL Server, который де­ простым и почти полностью лишенным великолепная база данных, но она не очень хорошо подходит для круп­ SQLite - номасштабных веб-приложений, а потому при развертывании приложения MVC требуется переход на другую базу данных. Изменения в конфигурации могут быть упрощены с ис­ пользованием средств конфигурирования, которые будут описаны в главе 14, но приложе­ ние должно быть тщательно протестировано в испытательной среде, чтобы выявить любые отличия, привносимые производственной базой данных. https: / /www. sqli te. org /whentouse. html, если вы не SQLite в производственной среде. Там приведен обзор ситуа­ SQLite может быть хорошим вариантом, а когда нет. Почитайте статью по адресу уверены в том, применять ли ций, когда база данных SQLite не поддерживает полный набор измене­ Entity Framework Core способна генерировать для дру­ проблема, когда SQLite используется при разработке, пос­ Важно принимать во внимание тот факт, что ний схемы, которые инфраструктура гих баз данных. В целом это не кольку вы можете удалить файл базы данных и сгенерировать новый файл с чистой схемой. Тем не менее, ситуация усложняется, если вы обдумываете развертывание приложения, в котором применяется SQLite. Если вы хотите использовать ту же самую базу данных в среде разработки и в производствен­ Entity Framework https: / /docs .microsoft. com/ru-ru/ef/core/ момент написания главы список был относительно коротким, но в Microsoft ной среде, тогда ознакомьтесь со списком поддерживаемых инфраструктурой Соге баз данных по адресу providers/. На объявили о поддержке баз данных, которые больше подходят для процесса разработки, чем SQLite, и могут функционировать также на платформах, отличающихся от Windows. Добавление пакета базы данных Пакет NuGet, содержащий инструменты командной строки, которые используются для создания и применения миграций базы данных, должен быть добавлен в проект вручную. Откроем файл ный в листинге Листинг 13.22. 13.22 Partyinvi tes. csproj и добавим в него элемент, выделен­ полужирным. Добавление пакета из папки NuGet Partyinvi tes в файле Partyinvi tes. csproj <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <DotNetCliToolReference Include="Мicrosoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup> </Project> Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 372 Сохраним внесенные изменения и. находясь в папке манду, показанную в листинге 13.23, Partyinvi tes, запустим ко­ чтобы обеспечить загрузку и установку нового пакета. Листинг 13.23. Установка пакета NuGet dotnet restore Создание и применение миграции базы данных Процедура создания базы данных следует тому же самому процессу. который ис­ пользуется в Vlsual Studlo. ним команду из листинга Листинг 13.24. Чтобы создать начальную миграцию базы данных, выпол­ 13.24, находясь в папке Partyinvi tes. Создание начальной миграции базы данных dotnet ef migrations add Initial Инфраструктура Entlty Framework Core создаст папку по имени М i g r а t i on s, содержащую файлы классов С#, которые будут использоваться для настройки схе­ мы базы данных. Чтобы применить эту миграцию базы данных, запустим в папке Partyinvites команду Partyinvites. из листинга 13.25, которая создаст базу данных внутри пап­ ки Листинг 13.25. Применение миграции базы данных dotnet ef database update Visual Studio Code не включает поддержку для инспектирования баз данных http: //sqlitebrowser.org можно найти великолепный инструмент с открытым кодом для Windows. macOS и Llnux. Среда но на веб-сайте SQLite. Создание контроллеров и представлений В настоящем разделе мы добавим в приложение контроллер и представления. Partyinvi tes/Controllers и добавления в нее файла HomeController. cs с определением, показанным в листинге 13.26. Начнем с создания папки класса по имени Листинг 13.26. Содержимое файла из папки using using using using HomeCon troller. cs Partyinvi tes/Controllers System; Microsoft.AspNetCore.Mvc; Partyinvites.Models; System.Linq; namespace Partylnvites.Controllers puЫic class HomeController : Controller private IRepository repository; HomeController(IRepository repo) => this.repository = repo; puЫic Глава 13. Работа с Visual Studio Code ViewResult Index() { int hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" return View ( "MyView"); 373 puЫic "Good Afternoon"; [HttpGet) ViewResul t Rsvpform () => View (); puЫic [HttpPost) ViewResult Rsvpform(GuestResponse guestResponse) if (ModelState. IsValid) { repository.AddResponse(guestResponse); return View("Thanks", guestResponse); else { puЫic //Имеется ошибка проверки достоверности return View(); ViewResult ListResponses() => View(repository.Responses.Where(r => r.WillAttend puЬlic true)); Для установки встроенных вспомогательных функций дескрипторов создадим пап­ ку Partyinvi tes/Views и добавим в нее файл по имени_ Viewimports. торый содержит выражение, приведенное в листинге Листинг cshtml, ко­ 13.27. 13.27. Содержимое файла_Viewimports. cshtml из папки Partyinvi tes/Views @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Partyinvites/Views/Home и добавим в нее файл по MyView. cshtml. в котором определяется представление, выбираемое мето­ дом действия Index () из листинга 13.26. Поместим в него разметку, показанную в листинге 13.28. Далее создадим папку имени Листинг 13.28. Содержимое файла МyView. cshtml из папки Partyinvi tes/Views/Home @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" cor1tent="width=device-width" /> <title>Index</title> <link rel="stylesheet" href="/liЬ/bootstrap/dist/css/bootstrap.css" /> </head> <body class="p-2"> <div class="text-center"> 374 Часть 1. Введение в инфраструктуру ASP.NET Соге MVC 2 <hЗ>We're going to have an exciting party!</hЗ> <h4>And you are invited</h4> <а class="btn btn-primary" asp-action="RsvpForm">RSVP Now</a> </div> </body> </html> Добавим в папку Part.yinvites/Views/Home с содержимым, приведенным в листинге 13.29. файл по имени RsvpForm.cshtml Это представление предлагает НТМL­ форму, которая будет заполняться пользователем, чтобы принять или отклонить при­ глашение на вечеринку. Листинг 13.29. Содержимое файла RsvpForm. cshtml из папки Partyinvi tes/Views/Home @model Partyinvites.Models.GuestResponse @( Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RsvpForm</title> <link rel="stylesheet" href="/liЬ/bootstrap/dist/css/bootstrap.css" /> </head> <body> <div class="m-2"> <div class="text-center"><h4>RSVP</h4></div> <form class="p-1" asp-action="RsvpForm" method="post"> <div asp-validation-summary="All"></div> <div class="form-group"> <label asp-for="Name">Your name:</label> <input class="form-control" asp-for="Name" /> </div> <div class="form-group"> <label asp-for="Email">Your email:</label> <input class="form-control" asp-for="Email" /> </div> <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-coпtrol" asp-for="WillAtteпd"> <optioп value="">Choose an option</option> <option value="true">Yes, I'll Ье there</option> <option value="false">No, I can' t come</option> </select> </div> Глава 13. Работа с Visual Studio Code 375 <div class="text-center"> <button class="btn btn-primary" type="submit"> Submit RSVP </button> </div> </form> </div> </body> </html> Следующий файл представления называется папке Partyinvi tes/Views/Home Thanks. cshtml, также создается в и имеет содержимое, показанное в листинге 13.30, которое отображается. когда гость отправляет свой ответ. Листинг 13.30. Содержимое файла из папки Thanks. cshtml Partyinvi tes/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 <а asp-action="ListResponses">here</a> to see who is coming. </body> </html> Финальный файл представления имеет имя ListResponses. cshtml и подобно Partyinvites/Views/Home. другим представлениям в примере добавляется в папку Он отображает список ответов гостей, используя разметку из листинга Листинг 13.31. Содержимое файла из папки @model ListResponses. cshtml Partyinvi tes/Views/Home IEnumeraЬle<Partyinvites.Models.GuestResponse> @{ Layout = null; 13.31. 376 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 < ! DOCTYPE html> <html> <head> <meta name = " viewport " content="width=device- widt h" /> <l ink rel="stylesheet" href=" / l i Ь/bootstrap /dist /css / b oo t strap.c ss" /> <title>Responses</title> </he ad > <body> <div class="m-1 р-1" > <h2>Here is the list of people attending the part y</h 2> <tаЫе clas s = "taЫe taЬle-sm taЬle-striped taЫe -bord ered"> <thead> <tr><th>Name</th><th>Emai l< /th><th>Ph one< / th></tr > </thead> <t body> @foreac h (Partyinvites .Mode ls. Gue s t Response r in Mo del ) { <t r ><td>@r.Name</td> <td>@r.Email </ td>< td >@r.Phone</td>< / tr > </tbody> < /tаЫе > </div> </bod y > </ html> Выполним команду dotnet run в папке проект и запустить исполняющую среду приложение можно , перейдя на Partylnvi te s, чтобы скомпилировать Увидеть в работе завершенное ASP.NET Саге. (рис. http : / / localhost: 50 0 0 13. 10). RSVP We're goi11g to have an е) ~~ ' • • Thank you, '1 t ;6 .~ tl'. "". ВоЫ Here is the list of people attending the party Рис. 13.10. Процес с модульного тестирования в Visual Studio. ...... 666-12.]..4 i)QЬ~mpi~C('(n \SS·-4'567 Выполнение завершенного приложения Модульное тестирование в процесс в ....... Жl!'t:~«nplrШ'fl Visual Studio Code Visual Studio Code похож на аналогичный П ервым делом понадобится создать отдельный проект для модульного тестирования. Глава 13. Работа с Visual Studio Code Создадим внутри папки InvitesProject папку по имеЮ1 Tests ду. представленную в листинге Листинг 13.32. 13.32, 377 и запустим в ней коман­ которая создаст проект модульного тестирования. Создание проекта модульного тестирования dotnet new xunit --language Запустим в папке Tests С# --framework netcoreapp2.0 команду из листинга 13.33, чтобы добавить ссылку на про­ ект приложения, сделав содержащиеся в нем классы доступными для тестирования. Листинг 13.33. Добавление ссылки на проект приложения dotnet add reference .. /Partylnvites/Partylnvite s.csproj Создание модульного теста Модульные тесты создаются так, как было описано в главе 7. Добавим в папку Tests новый файл класса по имени HomeControllerTests. cs с содержимым, пока­ занным в листинге Листинг 13.34. 13.34. Содержимое файла HomeControllerTests. cs из папки Tests using System; using System.Collections.Gener ic; using Partylnvites.Controllers; using Partylnvites.Models; using Xunit; using Microsoft.AspNetCore.Mvc ; using System.Linq; namespace Tests { puЫic class HomeControllerTests [Fact] puЫic void ListActionFiltersNonAtten dees() 11 Организация HomeController controller = new HomeController(new FakeRepository()); 11 Действие ViewResult result = controller.ListResponses (); //Утверждение Assert.Equal(2, (result.Model as IEnumeraЫe<GuestResponse>) class FakeRepository : IRepository { puЫic IEnumeraЫe<GuestResponse> new List<GuestResponse> { new GuestResponse { Name new GuestResponse { Name new GuestResponse { Name Responses => WillAttend = true } , "Alice", WillAttend = true } , false } "Joe", WillAttend "БоЬ", }; void AddResponse(GuestResponse response) throw new NotlmplementedException( ); puЬlic .Count()); 378 Часть 1. Введение в инфраструктуру ASP.NET Core MVC 2 Мы создали стандартный тест xUnit, который проверяет действие в контроллере Ноте и корректно фильтрует в хранилище объекты значением свойства WillAttend, равным ListResponses GuestResponse со false. Прогон тестов Чтобы прогнать модульные тесты в проекте, запустим в папке приведенную в листинге Листинг 13.35. Tests команду, 13.35. Прогон модульных тестов dotnet test Все тесты в проекте выполнятся. в результате чего отобразится вывод вроде пока­ занного ниже: Starting test execution, please wait ... [xUnit.net 00:00:00.6731479) Discovering: [xUnit.net 00:00:00.7900132] Discovered: [xUnit.net 00:00:00.8432715] Starting: [xUnit.net 00:00:00.9967614] Finished: Tests Tests Tests Tests Total tests: 2. Passed: 2. Failed: О. Skipped: Test Run Successful. Test execution time: 1.6974 Seconds О. В выводе присутствуют результаты двух тестов. поскольку шаблон проекта вклю­ чает файл по имени UnitTestl.cs, который содержит пустой модульный тест. Упомянутый файл можно удалить, как демонстрировалось в главе 7. Резюме В главе был предложен краткий обзор работы со средой Visual Studio Code - лег­ ковесным инструментом разработки, который поддерживает создание приложений ASP.NEТ Core MVC в Windows, macOS и Linux. Среда Visual Studio Code пока еще не яв­ Visual Studio, но предоставляет основные средства, построении приложений МVС, и расширяется компанией Microsoft ляется заменой полного продукта необходимые при в ежемесячных выпусках. Итак. первая часть книги завершена. Во второй части мы начнем погружение в детали и посмотрим. как работают средства, которые использовались для создания примера приложения. ЧАСТЬ 11 Подробные сведения об инфраструктуре ASP.NET Core МVС 2 моменту вы уже знаете, для чего существует инфраструкту­ к настоящему ра .NET Саге MVC, а также понимаете ее архитектуру и лежащие в основе проектные цели. Вы получили эти знания благодаря построению реалистичного приложения электронной коммерции. Наступило время заняться исследованием внутреннего устройства инфраструктуры. Во второй части книги мы погрузимся в детали, начав с изучения структуры при­ ложения ASP.NET Саге MVC и способа обработки запросов. Затем мы сосредо­ точим внимание на индивидуальных средствах, таких как маршрутизация, конт­ роллеры и действия, система представлений MVC и вспомогательных функций дескрипторов, а также особенности работы с моделями предметной области. ГЛАВА 14 Конфигурирование v приложении она открывает много ас­ т ема конфигурации может показаться неинтересной, но MVC пектов, связанных с функционированием приложений и обработкой НТГР­ запросов. Вы не должны поддаваться соблазну пропустить текущую главу. Вы обя­ заны выделить время на изучение способа. которым система конфигурации придает форму веб-приложениям MVC. Данная тема образует прочный фундамент для пони­ мания материала последующих глав. MVC, и демонстрируется. MVC опирается на средства. предоставляемые плат­ 14.1 приведена сводка. позволяющая поместить конфи­ В главе объясняется, как конфигурировать приложения каким образом инфраструктура формой ASP.NET Core. В табл. гурирование приложений в контекст. Таблица 14.1 . Помещение системы конфигурации в контекст Вопрос Ответ Что это такое? Классы Program и Startup и файлы JSON используются для конфигурирования работы приложения, а также указания паке­ тов, от которых оно зависит Чем она полезна? Система конфигурации позволяет подстраивать приложения под их среды и управлять зависимостями от пакетов Как она используется? Самым важным компонентом является класс Startup, кото­ рый применяется для создания служб (объектов, предостав­ ляющих общую функциональность повсюду в приложении) и компонентов промежуточного программного обеспечения (ПО), используемых для обработки НТТР-запросов Существуют ли какие-то В сложных приложениях конфигурация может стать трудной в скрытые ловушки или управлении. В разделе "Работа со сложными конфигурациями" ограничения? далее в главе описаны средства ASP.NET, предназначенные для решения такой проблемы Существуют ли Нет. Система конфигурации альтернативы? и средство настройки приложений В табл. 14.2 представлена сводка по главе. - это неотъемлемая часть ASP.NET MVC Глава 14. Конфигурирование приложений Таблица 14.2. Сводка по главе Задача Решение Добавление функциональности в Добавьте пакеты Листинг NuGet приложение .csproj Управление инициализацией прило­ Используйте класс жения 381 в файл 14.5-14.8 14.9-14.11 Program ASP.NET Конфигурирование приложения 14.12, 14.13 Применяйте методы ConfigureServices() и Configure () класса Startup Создание общей функциональности 14.14-14.16 Используйте метод ConfigureServices () для создания служб Генерация ответов с содержимым Создайте промежуточное ПО для 14. 17-14. 19 генерации содержимого Предотвращение прохождения за­ Создайте промежуточное ПО для просов через конвейер запросов обхода Редактирование запроса перед его Создайте промежуточное ПО для обработкой другими компонентами редактирования запросов 14.20, 14.21 14.22-14.24 промежуточного ПО Редактирование ответа, который был обработан другими компонентами Создайте промежуточное ПО для 14.25, 14.26 редактирования ответов промежуточного ПО Применяйте метод UseMvc () или UseMvcWithDefaultRoute() 14.27 Изменение конфигурации приложе­ Используйте службу среды 14.28 ния для разных сред размещения Настройка функциональности MVC Обработка ошибок в приложении Используйте промежуточное ПО для 14.29, 14.30 обработки ошибок в среде разработки или производственной среде Управление несколькими браузерами Применяйте средство во время разработки (Ссылка на браузер) Включение файлов изображений, JavaScript и CSS Browser Link Включите промежуточное ПО для 14.32 статического содержимого Отделение данных конфигурации Создайте внешние источники кон- от кода С# фигурации, такие как файлы Регистрация данных приложения Применяйте промежуточное ПО для в журнале регистрации в журнале Подготовка системы внедрения зави­ Отключите проверку области симостей для использования действия с 14.31 14.33-14.35 JSON 14.36-14.38 14.39 Entity Framework Core Конфигурирование служб MVC Используйте средства параметров 14.40 кон фигурации Конфигурирование сложных Применяйте несколько внешних приложений файлов или классов 14.41-14.45 382 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Подготовка проекта для примера В текущей главе мы создадим новый проект по имени зованием шаблона Empty ConfiguringApps с исполь­ (Пустой). Мы будем конфигурировать приложение позже в главе, но есть несколько базовых средств. которые необходимо поместить на свои мес­ та в плане подготовки к предстоящим изменениям. Мы собираемся применять му создадим файл Bower), (Файл конфигурации Листинг 14.1. Bootstrap bower. j son, Добавление для стилизации НТМL-содержимоrо, поэто­ используя шаблон элемента Bower Configuration File 14.1. и добавим в него пакет, как показано в листинге Bootstrap в файле bower. j son из папки ConfiguringApps "name": "asp.net", "private": true, "dependencies": { "bootstrap": "4. О. 0-alpha. 6 11 Создадим папку Controllers HomeController. cs Листинг 14.2. и поместим в нее файл класса по с определением контроллера. приведенным в листинге Содержимое файла HomeController.cs из папки имени 14.2. Controllers using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; namespace ConfiguringApps.Controllers puЫic class HomeController : Controller ViewResult Index() => View(new Dictionary<string, string> ["Message"] = "This is the Index action" puЬlic }) ; Создадим папку Index. cshtml Views/Home и добавим в нее файл представления по имени с содержимым. показанным в листинге Листинг 14.З. Содержимое файла Index. cshtml 14.3. из папки Views/Home @model Dictionary<string, string> @{ Layout = null;} < ! DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link asp-href-include="liЬ/bootstrap/dist/css/*.min.css" rel="stylesheet" /> <title>Result</title> </head> Глава 14. Конфигурирование приложений 383 <body class= "p- 1" > < tаЫе class="taЫe taЫe- co nde n sed taЫe- border ed taЫ e-stripe d" > @foreach (var kvp in Model ) { <tr><th>@kvp.Key</th><td>@kvp.Value</td>< / tr> < / tаЫе> </body> </html > При выборе СSS-файлов Bootstrap элемент link в представлении полагается на встроенную вспомогательную функцию дескриптора. Чтобы включить встро­ Vi ews файл MVC View lmports Page енные вспомогательные функции дескрипторов . со здадим в папке _ Vie wlmpo rts . csh tml с применением шаблона элемента (Страница импортирования представлений листинга Листинг MVC) и поместим в него выражение из 14.4. 14.4. Содержимое файла _ Viewimports. csh tml из папки Views @addTagHelper *, Microso ft . AspNetCor e.Mvc .TagHe l pe rs В результате запуска приложения отобразится сообщение. представленное на рис. 14.1 . CJ l0<•lhostб5417 С Х ф localhost 1>~41 , Hello World ! L Рис. 14. 1. Выполнение примера приложения Конфигурирование проекта cspro j . ко­ ASP.NET ConfiguringApp s . csproj . Самым важным конфигурационным файлом является < имя_ проекта > . торый заменяет файл proj ect . j son . используемый в более ранних версиях Core. Такой файл, который в проекте примера наз ывается среда Visual Studio скрывает и разрешает к нему доступ за счет щелчка правой кноп­ кой мыши на элементе проекта в окне Solution Explorer и выбора в контекстном меню пункта Edit ConfiguringApps.csproj (Редактировать Configur ingApps. cspro j). В лис­ тинге 14.5 приведено начальное соде ржимое файла Co nfigu ringApp s . cspro j, кото­ рое было создано Visual Studio как часть шаблона проекта Empty. Листинг 14.5. Содержимое файла из папки ConfiguringApps. csproj ConfiguringApps <P r oj ect Sdk="Microsoft.NET.Sdk.Web"> <Prope rtyGroup> <TargetFramework>ne t c oreapp 2.0 </Target Fr amewo rk> </PropertyGr o up> 384 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> </ItemGroup> </Project> . csproj Файл MSBuild, применяется для конфигурирования инструмента рый используется при построении проектов помощью элементов XML. а в табл. 14.3 .NET. кото­ Конфигурирование производится с описаны элементы. находящиеся в стандарт­ ном конфигурационном файле. В более поздних примерах будут применяться другие но, чтобы начать работу над проектом 14.3 элементов ASP.NET Core MVC. Таблица XML элементы конфигурации, но представленных в табл. 14.3. Элементы конфигурации в стандартном файле вполне достаточ­ . csproj Элемент Описание Project Это корневой элемент, который обозначает конфигурационный файл MSBuild. Атрибут Sdk устанавливается в Microsoft. NET. Sdk. Web, чтобы предоставить набор неявно импортируемых паке­ тов, требующихся для построения проекта PropertyGroup Этот элемент группирует связанные конфигурационные свойства в целях придания файлу структурированности TargetFramework Этот элемент указывает целевую платформу .NET Framework для процесса построения и должен быть определен внутри PropertyGroup. Стандартным значением netcoreapp2. О, направляющее на .NET Core 2.0 элемента ItemGroup является Этот элемент группирует связанные конфигурационные элементы в целях придания файлу структурированности Folder Этот элемент сообщает MSBuild о том, как иметь дело с той или 14.5 указывает иной папкой в проекте. Данный элемент в листинге инструменту MSBuild о необходимости включения папки wwwroot, когда приложение публикуется PackageReference Этот элемент используется для указания зависимости от пакета NuGet, который идентифицируется через атрибуты Include и Version. Пакет Microsoft. AspNetCore. All применяется для обеспечения доступа ко всем индивидуальным пакетам, которые предоставляют функциональность ASP.NET Core и MVC Framework Добавление пакетов в проект . csproj - составить Visua\ Studio обнаруживает Самая важная роль файла сит проект. Когда среда список пакетов, от которых зави­ изменение в файле . csproj. она инспектирует список пакетов, загружает любые вновь добавленные пакеты и удаляет любые пакеты. которые больше не нужны. С выпуском ASP.NET Core 2 вся базовая функциональность. требующаяся для ASP.NET Core MVC, MVC Fгamework и Entity Framework Core. включена в мета-пакет Глава 14. Конфигурирование приложений Microsoft. AspNetCore. All - 385 удобная возможность , которая позволяет избежать необходимости начинать разработку приложения с добавления в проект длинного списка пакетов NuGet. Но даже при таких условиях по-прежнему придется добавлять пакеты NuGet для сторонних или расширенных функциональных возможностей. Существует три спосо­ - выбор пункта меню Toolsq NuGet NuGet Packages for Solutioп (Сервисq Диспетчер пакетов пакетами NuGet для решения). что позволит управлять пакетами ба добавления пакетов в проект. Первый способ Package Maпagerq Мапаgе NuGеtqУправлять NuGet через легкий в использовании интерфейс. Для новичков в мире разработки приложений .NET такой подход будет наилучшим, потому что он снижает вероят­ ность ошибки при выборе пакета. Например, чтобы добавить пакет System. Net. Ht tp, который обеспечивает под ­ держку отправки (а не получения) запросов НТТР, можно перейти на вкладку (Обзор) окна диспетчера пакетов NuGet, список доступных версий, в том числе предварительных версий (рис . lnstalled Updates Browse произвести поиск по имени и увидеть полный 14.2). Manage Packages for Solution Consolidate 13.1М .S......lo•d• Pro\1dts 1 progr"mmmg inttrf4<tfoc modttn НТТР "ppftotions. 1ncludingНTTP clttnt compon.ntJ thlt .Моw 1pphat.юns. to consumt wtЬ strvicб ovtt НТТР and НПР cc.w System.Net.Http ЬyMkroюf\ \'<J System.Net.Primitlves Ьу Мюоюfl. 114.З.О 12.4М downlo•:h Provida common typ~ for networ\::·b•Ш imra1~. 1rкludlng Systf.m..Net.19AddrltS<;. Sysltm.Нtt IPE.ndPoН\r,. 1nd Sy~tm.Nd Coo~ieCon1JW'ltf, System.Not.Sockets Ьу M1ao'°ft, 8.6 1М d""'nlo•d• PtovIOts cl1sS6 wch 1s Sod.et. ТcpOttnt and UdpCl1Мl., wh1ch tnгblt devtlopeis to stnd 1nd tt<.t1lf't d.rt• over the networt. v4.3.0 runtime.native.-System.Net.Ht1p Ьу Mкrosoft. 6.09М downlo1ds -AJ.O lnttm.111 m'lplem1ntation pг<Qgt nct munt fOf d11t.ct conwmpnon . Plost do 1\ot rtftttrк.t dirюly. Syst•m.Net.Http.Formarting.Ext•nsion Ьу •nd«.'<J°"'""° 687К d""nlo.od vi.2.3 lnstьll bltiten A.s:.1.rnblySysttm.Ne-t.Нttp, Form•tt1ng.dO .m et.Ftp 1;,,nt.A :ync tr Рис. 14.2. Добавление пакета с применением диспетчера пакетов NuGet Далее необходимо выбрать пакет интересующей версии, указать проект, для ко­ торого требуется этот пакет, и щелкнуть на кнопке lпstall (Установить). Среда Studio загрузит пакет и обновит файл Visual . csproj . Добавлять пакеты можно также с использованием командной строки. хотя в таком случае нужно знать имя пакета (и в идеале его версию) . В листинге манда. которую пришлось бы выполнить, находясь в папке добавить в проект пакет Листинг System. Net. Http. 14.6. Добавление пакета в проект dotnet add pa c kage System .Net .H ttp -- version 4 . 3 .2 14.6 показана ко ­ Configur i.ngApps, чтобы 386 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC 2 Как диспетчер пакетов менты NuGet, так и команда dotnet add package добавляют эле­ PackageReference в файл . csproj. При желании пакеты можно добавлять за счет редактирования конфигурационного файла, вручную добавляя в него элемен­ ты PackageReference. Подход является самым прямым, но требует внимательности во избежание неправильного набора имен пакетов или указания для них несуществу­ ющих номеров версий. В листинге Nеt.Нttрвфайле Листинг 14. 7. 14.7 демонстрируется добавление пакета System. .csproj. Добавление пакета в файле ConfiguringApps. csproj из папки ConfiguringApps <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <PackageReference Include="System.Net.Http" Version="4.3.2" /> </ItemGroup> </Project> Элемент PackageReference имеет атрибут Include, Version, где задается номер версии. в котором указывается имя пакета, и атрибут Добавление пакетов инструментов в проект В то время как обычные пакеты допускается добавлять различными способами, оп­ ределенные пакеты расширяют диапазон задач, которые можно выполнять с помощью инструмента командной строки . csproj dotnet, и такие пакеты требуют применения в файле элемента другого вида. Речь идет об элементе указываемом вместо элемента PackageReference, DotNetCli ToolReference, который используется пакетами средств. применяемых приложением напрямую. Пакеты подобного рода могут добав­ ляться в проекты только путем непосредственного редактирования файла В листинге 14.8 к проекту добавляется . csproj. пакет, который делает возможным создание и применение миграций базы данных с использованием команд dotnet ef, как было показано в первой части книги. Листинг 14.8. Добавление пакетов инструментов в файле ConfiguringApps. csproj из папки ConfiguringApps <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <Folder Include="wwwroot\" /> </ItemGroup> Глава 14. Конфигурирование приложений 387 <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <PackageReference Include="System.Net.Http" Version="4.3.2" /> <DotNetCliToolReference Include="Мicrosoft.EntityFra.iaeworkCore.Tools.DotNet" Version="2.0.0 </ItemGroup> 11 /> </Project> При добавлении пакетов инструментов в проект можно либо поместить элемен­ DotNetCli ToolReference в тот же самый элемент ItemGroup, где находятся PackageReference, что и было сделано в листинге 14.8, либо создать отдельный элемент ItemGroup. После сохранения изменений, внесенных в файл . csproj, среда Visual Studio загрузит и установит пакеты и задействует их для конфигурирования инструмента командной строки dotnet. ты обычные элементы Класс Класс Program Program определяется в файле по имени Program. cs и предоставляет точ­ ку входа для запуска приложения, снабжая инфраструктуру .NЕТ методом Main (). который может быть выполнен для конфигурирования среды размещения и выбора класса. завершающего конфигурирование приложения содержимого класса тов: в листинге 14.9 Program ASP.NET Core. Стандартного вполне достаточно для разработки большинства проек­ приведен стандартный код, который среда Visual Studio добав­ ляет в проекты. Листинг using using using using using using using using using 14.9. Стандартное содержимое файла Progra.ia.cs из папки System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; namespace Conf iguringApps { puЫic class Program { static void Main ( string [] args) { BuildWebHost(args) .Run(); puЫic static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build(); puЫic ConfiguringApps 388 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Метод ложения метода обеспечивает точку входа. которую должны предоставлять все при­ Main () .NET, чтобы иметь возможность выполняться исполняющей средой. Внутри Main () в классе Program вызывается метод BuildWebHost (),который несет ответственность за конфигурирование ASP.NET Core. Для конфигурирования ASP.NEТ Core метод BuildWebHost () применяет статичес­ кие методы. определенные в классе WebHost. С выходом ASP.NET Core 2 конфигури­ рование упростилось за счет появления метода конфигурирует ASP.NET Core проектов. Метод CreateDefaul tBuilder (),который с использованием настроек. подходящих большинству UseStartup () вызывается с целью идентификации класса. кото­ рый будет предоставлять конфигурацию, специфичную для приложения: по соглаше­ нию применяется класс по имени интерфейс Run ( ) , Startup. рассматриваемый позже в главе. Метод обрабатывает все настройки конфигурации и создает объект. реализующий Build () IWebHost и возвращаемый методу Main (),на котором вызывается метод чтобы начать обработку НТТР-запросов. Анализ деталей конфигурации Метод ции CreateDefaul tBuilder () ASP.NET Core. является удобной отправной точкой конфигура­ но он скрывает много важных деталей, что может стать проблемой, если необходимо изменить способ, которым конфигурируется приложение. В листин­ ге 14.10 вызов метода CreateDefaul tBuilder () заменен отдельными операторами. создающими стандартную конфигурацию. Листинг 14.1 О. Детальные операторы конфигурирования в файле из папки using using using using using using using using using using Program. cs ConfiguringApps System; System.Collections.Generic; System. IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps { puЫic class Program { static void Main ( str ing [) args) { BuildWebHost(args) .Run(); puЫic static IWebHost BuildWebHost(string[] args) { return new WeЬHostBuilder() . UseKestrel () .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; confiq. AddJsonFile ( "appsettings. j son" , optional: true, reloadOnChange: true) .AddJsonFile($ 11 appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChanqe: true); puЫic Глава 14. Конфигурирование nриложений 389 if (env.IsDevelopment()) { var appAssemЫy = AssemЫy.Load(new AssemЬlyName(env.ApplicationName)); if (appAssemЬly != null) { config.AddUserSecrets(appAssemЬly, optional: true); config.AddEnvironmentVariaЬles(); if (args ! = null) { config.AddCommandLine(args); } }) .ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration( hostingContext.Configuration.GetSection("Logging ")); logging.AddConsole(); logging.AddDebug(); }) .UseIISintegration() .UseDefaultServiceProvider((context, options) => options. ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build(); Все методы конфигурирования, добавленные в метод описаны в табл. Таблица BuildWebHost (), кратко 14.4. 14.4. Стандартные методы конфигурирования ASP.NET Core Имя Описание UseKestrel () Этот метод конфигурирует веб-сервер няется ниже во врезке "Использование UseContentRoot() Kestrel, как объяс­ Kestrel напрямую" Этот метод конфигурирует корневой каталог для прило­ жения, который применяется для загрузки файлов кон­ фигурации и доставки статического содержимого, такого как файлы изображений, ConfigureAppConfiguration() JavaScript и CSS Этот метод используется для подготовки конфигура­ ционных данных приложения, как описано в разделе "Конфигурирование приложения" далее в главе AddUserSecrets () Этот метод применяется для хранения конфиденциаль­ ных данных за пределами файлов кода; подробные све­ дения доступны по ссылке https://docs.rnicrosoft.com/ru-ru/aspnet/ core/security/app-secrets?view=aspnetcore2. l&tabs=windows. Средство несколько неуклюжее и потому оно в книге не используется 390 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Окончание табл. 14.4 Имя Описание ConfigureLogging() Этот метод позволяет сконфигурировать регистрацию в журнале для приложения, как объясняется в разделе "Конфигурирование регистрации в журнале" позже в главе UseIISintegration() Этот метод включает интеграцию с UseDefaultServiceProvider() llS и llS Express Этот метод применяется для конфигурирования внедрения зависимостей, как описано в разделе "Конфигурирование внедрения зависимостей" далее в главе UseStartup () Этот метод указывает класс, который будет использо­ ваться для конфигурирования ASP.NEТ, как описано в разделе "Класс Startup" далее в главе Позже в главе бу.цут объясняться более сложные операторы из листинга 14.10. а пока мы удалим некоторые операторы конфигурирования, чтобы оставить только базовую конфигурацию (листинг Листинг using using using using using using using using using using 14.11. Упрощение 14. l l). конфиrурации в файпе Program. cs из папки ConfiguringApps System; System.Collections.Generic; System. 10; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps { puЫic class Program { static void Main(string[] args) { BuildWebHost(args) .Run(); puЫic static IWebHost BuildWebHost(string[] args) ( return new WeЬHostвuilder () . UseKestrel () .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISintegration() .UseStartup<Startup>() . Build (); puЫic Показанные в листинге 14. l l операторы обеспечивают базовую конфигурацию, которая будет работать для большинства приложений ASP.NET Core MVC. Другие опе­ раторы будут добавлены снова по мере объяснения функциональных средств, к кото­ рым они относятся. Глава 14. Конфигурирование приложений Использование Kestrel 391 напрямую Kestrel - это межплатформенный веб-сервер, спроектированный для выполнения приложе­ ний ASP.NET Саге. Он задействуется автоматически, когда запускается приложение ASP.NET Core с применением llS Express (сервер, предоставляемый средой Visual Studio для исполь­ зования на стадии разработки) или полной версии llS, которая была традиционной веб­ платформой для приложений .NET. При желании веб-сервер Kestrel также разрешено запускать напрямую, что означает воз­ можность выполнения приложений ASP.NET Core MVC на любой из поддерживаемых плат­ Windows, которое накладывает llS. Существуют два способа запуска приложения с применением Kestrel. Первый из них щелчок на стрелке рядом с правой гранью кнопки llS Express в панели инструментов Visual Studio и выбор элемента, который соответствует имени проекта. В результате откроется новое окно командной строки, и приложение запустится с использованием Kestrel. форм, обходя ограничение только операционной системой Той же цели можно достичь, открыв собственное окно командной строки, перейдя в папку с файлами конфигурации приложения (ту, которая содержит файл . csproj) и запустив сле­ дующую команду: dotnet run По умолчанию веб-сервер Kestrel начинает прослушивать порт 5000 на предмет поступле­ ния НТТР-запросов. Если в проекте присутствует файл j son, Properties/launchSettings. тогда настройки порта НТТР и среды для приложения будут читаться из него. Класс Класс Startup Program отвечает за быстрое начало функционирования приложения, но бо­ лее важная работа по конфигурированию делегируется через метод UseStartup (): .UseStartup<Startup>() При идентификации класса, который будет конфигурировать ASP.NET Саге. метод UseStartup () полагается на параметр типа. По соглашению этот класс имеет имя Startup, которое применяется шаблонами проектов ASP.NET Core МVС. в том числе шаблоном Empty, используемым для создания проекта примера в текущей главе. Изучение работы класса Startup позволяет уловить суть способа обработки НТТР­ MVC с остальными частями платформы запросов и интеграции инфраструктуры ASP.NET Core. В настоящем разделе мы начнем с простейшего из возможных классов Startup и добавим средства для демонстрации влияния различных конфигурационных па­ раметров, получив в итоге конфигурацию. которая будет подходящей в большинстве проектов MVC. Visual Studlo функциональность, необходимую Листинг 14.12. 14.12 показан класс Startup, Empty, который настраивает всего лишь В качестве отправной точки в листинге добавляемый средой к проектам ASP.NET Core для обработки НТТР-запросов. Начальное содержимое файла using System; using System.Collections.Generic; using System.Linq; Startup. cs из папки ConfiguringApps 392 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 using using using using using System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace ConfiguringApps { puЫic class Startup { puЬlic void ConfigureServices(IServiceCollection services) { ) puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); ) ) ; В классе Startup определены два метода, ConfigureServices () и Configure (), которые настраивают совместно используемые средства. требующиеся приложению. и указывают ASP.NEТ о том, как они должны применяться. Когда приложение запускается. инфраструктура ASP.NET Соге создает новый эк­ земпляр класса Startup и вызывает его метод ConfigureServices (), так что при­ ложение может создать свои службы. Как объясняется в разделе ··службы ASP.NEТ" далее в главе. службы - это объекты. которые предоставляют функциональность дру­ гим частям приложения. Приведенное описание не отличается особой строгостью по той причине. что службы могут использоваться для снабжения практически любой функциональностью. После того как службы созданы, метода ASP.NET вызывает метод Configure (). Целью является настройка конвейера запросов, который представляет Configure () собой набор компонентов (называемых промежуточным программным обеспечени­ ем), используемых для обработки входящих НТТР-запросов и генерации ответов на них. В разделе "Промежуточное программное обеспечение ASP.NET' далее в главе бу­ дут даны объяснения. как работает конвейер запросов, и продемонстрировано созда­ ние компонентов промежуточного ПО. На рис. ASP.NET с классом Приложение запускается Рис. 14.3 иллюстрируется взаимодействие Startup. Создается экземпляр класса 14.3. Startup Вызывается метод Configure Services () Применение класса Вызывается Начинается метод обработка Configure () запросов Службы Конвейер созданы подготовлен Startup инфраструктурой для конфигурирования приложения ASP.NET Глава 14. Конфигурирование приложений 393 Класс щение Startup, который для всех запросов просто возвращает одно и то же сооб­ "Hello Wo r l d 1 ",не особенно полезен. поэтому перед подробным обсуждением методов класса имеет смысл забежать чуть вперед и включить Листинг using using using usi n g u sing using u sing using MVC (листинг 14.13). 14.13. Включение MVC в файле Startup. cs из папки ConfiguringApps System; System.Collections.Gene ric; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builde r ; Microsoft. AspNetCore.Hos ti ng ; Microsoft .AspNetCore.Htt p; Microsoft.Extensions.Depende ncyinjecti on ; namespace ConfiguringApps { puЫi c c l ass St a rtup { puЫic void Co nfigureServices ( IServiceColle ct ion services ) { services.AddМvc(); puЫic void Configure(IApplica ti onBuilder app.UseMvcWithDefaultRoute(); арр, IHostingEnvironmen t env ) { Благодаря таким добавлениям (которые объясняются в последующих разделах) в нашем распоряжении оказывается достаточный объем инфраструктуры , чтобы об­ рабатывать НТТР-запросы и генерировать ответы с использованием контроллеров и представлений. На рис. 14.4 показан результат запуска приложения . D Result С 1 1 х Ф localhost '416 :Мessage Tl1is is the Index actioн L _____ - - - Рис. 14.4. Результат включениR MVC Обратите внимание. что содержимое не стилизовано. Минимальная конфигурация в листинге 14. 13 не предлагает ка кую -либо поддержку для обслуживания статическо­ го соде ржимого. такого как таблицы стилей 1i11 k CSS и файлы JavaScript. В в НТМL-разметке, визуализируемой представлением Iпdex. ет запрос к таблице стилей CSS из Bootstrap, итоге элемент csh trnl, иницииру ­ который приложение не в состоянии об­ работать. что предотвращает получение требуемой стилевой информации. Проблема будет устранена в разделе "Добавление оставшихся компонентов промежуточного программного обеспечения·· далее в гла ве. 394 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Службы ASP.NET Инфраструктура ASP.NET Core вызывает метод Startup. Conf igureServ ices (). так что приложение может настроить требуемые службы. Термин "служба" относится к любому объекту, который предоставляет функциональность другим частям приложе­ ния. Как уже отмечалось, такое описание не является строгим, поскольку службы спо­ собны делать для приложения все что угодно. В качестве примера создадим в проекте папку Infrastructure и добавим в нее файл класса по имени с определением, приведенным в листинге Листинг 14.14. Содержимое файла UptirneService. cs 14.14. UptimeService. cs из папки Infrastructure using System.Diagnostics; namespace ConfiguringApps.Infrastructure puЫic class UptimeService private Stopwatch timer; puЬlic UptimeService() { timer = Stopwatch.StartNew(); puЫic long Uptime => timer.ElapsedMilliseconds; Когда создается экземпляр класса UptimeService. его конструктор запускает тай­ мер, который отслеживает, сколько времени выполнялось приложение. Это хороший пример службы, потому что класс UptirneService обеспечивает функциональность, которая может применяться в остальных частях приложения и создается при запуске приложения. Службы класса ASP.NET регистрируются с использованием метода ConfigureServices () в листинге 14.15 показано. как зарегистрировать класс UptimeService. Startup; Листинг 14.15. Регистрация специальной службы в файле из папки using using using using using using using using using s tartup. cs ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { puЬlic void ConfigureServices(IServiceCollection services) ( services.AddSingleton<UptimeService>(); services.AddMvc(); Глава 14. Конфигурирование приложений void Configure (IApplicationBuilder app.UseMvcWithDefaultRoute(); puЫic Метод ConfigureServices () получает в IServiceCollection. реализует интерфейс 395 IHostingEnvironment env) { арр, качестве аргумента объект, который Службы регистрируются с помощью расширяющего метода, вызываемого на интерфейсе IServiceCollection, кото­ рому указываются разнообразные конфигурационные параметры. Доступные пара­ метры для создания служб будут описаны в главе AddSingleton (), UptimeService по 18, а пока мы применяем метод что означает совместное использование единственного объекта всему приложению. Службы тесно связаны со средством под названием внедрение зависимостей, ко­ торое позволяет компонентам вроде контроллеров легко получать службы и подробно рассматривается в главе 18. Получить доступ к службам, зарегистрированным в мето­ Startup. ConfigureServices (),можно за счет создания конструктора, который принимает аргумент с требуемым типом службы. В листинге 14.16 показан конструк­ тор, добавленный в контроллер Home для доступа к совместно используемому объекту UptimeService, который был создан в листинге 14.15. Кроме того, обновлен метод действия Index () контроллера. так что он включает в генерируемые данные пред­ ставления значение свойства Upda te службы. де Листинг 14.16. Доступ к службе в файле из папки HomeController.cs Controllers using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using ConfiguringApps.Infrastructure; namespace ConfiguringApps.Controllers puЫic class HomeController : Controller private UptimeService uptime; HomeController(UptimeService up) => uptime ViewResult Index() => View(new Dictionary<string, string> { [ "Message"] = "This is the Iлdex action", up; puЫic puЫic ["Uptime"] = $"{uptime.Uptime}ms" }) ; Когда инфраструктуре MVC необходим экземпляр класса контроллера обработки НТГР-запроса, она инспектирует конструктор живает, что ему требуется объект UptimeService. служб, которые были сконфигурированы в классе HomeController Затем MVC Startup. Home для и обнару­ инспектирует набор выясняет. что служба UptimeService сконфигурирована так, чтобы для всех запросов применялся единс­ твенный объект UptimeService, и передает этот объект в качестве аргумента конс­ труктору при создании экземпляра HomeController. Службы могут регистрироваться и потребляться более сложными способами, но рассмотренный пример продемонстрировал главную идею, лежащую в основе служб, и показал, как определение службы в классе Startup позволяет определять функци­ ональность или данные, которые используются по всему приложению. 396 Часть 11 . Подр о бные сведения об инфраструктуре ASP.NET Соге MVC 2 В результате запуска приложения и запроса ст андартного URL появится ответ, ко­ торый включает количество миллисекунд. прошедших с момента старта приложения (рис . 14.5). в классе Число миллисекунд получается из объекта Startup . Upt i meService, созданного (Строго говоря . отображаемое значение соответствует времени. прошедшему с момента создания объекта службы Up t i meServ ice. но оно довольно близко к моменту запуска приложения. чтобы для целей данной главы разница не играла никакой роли.) D Rrsult С l 1 )( <D localhost '5416 .Мessage Т11is is tl1e Iнdex actioн Uptime Рис. 5шs 14.5. Потребление простой службы Каждый раз . когда поступает з апрос к стандартному создает новый объект Uptime Serv i ce HomeC on tr o l le r URL. инфраструктура MVC и предоставляет ему разделяемый объект в виде аргумента конструктора. Это позволяет контроллеру Home по ­ лучать доступ к значению времени выполнения приложения. не заботясь о том, ка­ ким образом такая информация предоставлена или реализована . Службы MVC Пакет с уровнем сложности, как у MVC. имеет дело со многими службами; одни из них предназначены для внутреннего потребления. а другие предлагают функциональ­ ность разработчикам. Пакеты определяют расширяющие методы . которые настра­ ивают все требующиеся им службы в единственном вызове метода . Для метод имеет имя добавлены в AddMvc () и является одним из двух методов. класс Sta rt up , чтобы обеспечить работу MVC : MVC такой вызовы которых были void Con f i gu re Servic es( I ServiceCollection se r vice s ) { services .AddSi ngle t on<UptimeSe r vice>() ; puЫi c services.AddМvc(); Метод AddMvc () настраивает каждую службу. в которой Confi gu r eServ i ces () гигантским списком мождая метод На заметку! Средство lntelliSense других расширяющих методов, в Visual Studio которые нуждается MVC, не загро­ индивидуальных служб . будет отображать длинный список можно вызывать на объекте реализации ISer v i c eCo l l ec t i o n в методе Conf igureS e rvic e s () .Некоторые из этих мето­ Add Sin gl e ton () и AddScop ed () ,применяются для регистрации служб различными способами . Другие методы , подобные Add Rout in g () или AddCo rs (), до­ бавляют отдельные службы , которые уже использовались методом AddMvc () . В резуль­ тате для большинства приложений метод Con f igure Serv i ces () содержит небольшое число специальных служб , вызов метода AddMvc () и необязательно операторы конфи­ дов, такие как гурирования встроенных служб, которые описаны в разделе "Конфигурирование служб MVC" далее в главе. Глава 14. Конфигурирование nриложений Промежуточное программное обеспечение В ASP.NET Core 397 ASP.NET термин промежуточное ПО используется в отношении компонен­ тов. которые объединяются с целью формирования конвейера запросов. Конвейер запросов организован аналогично цепочке; когда поступает новый запрос, он пере­ дается первому компоненту промежуточного ПО в цепочке. Компонент инспектирует запрос и решает, обработать его и сгенерировать ответ или передать следующему ком­ поненту в цепочке. После того как запрос обработан, ответ, который будет возвращен клиенту, передается по цепочке обратно, что позволяет всем предшествующим ком­ понентам инспектировать или модифицировать его. Работа компонентов промежуточного ПО может выглядеть несколько странной, но она допускает большую гибкость в том, как приложения собираются вместе. Понимание того, каким образом применение промежуточного ПО придает форму при­ ложению, может оказаться важным, особенно если вы получаете не те ответы, кото­ рые ожидали. Чтобы выяснить, как функционирует система промежуточного ПО, мы создадим несколько специальных компонентов. которые продемонстрируют каждый из имеющихся четырех типов промежуточного ПО. Создание промежуточного по для генерации содержимого Самый важный тип промежуточного ПО генерирует содержимое для клиентов, и именно к данной категории принадлежит MVC. Чтобы создать компонент промежу­ точного ПО для генерации содержимого, не имея дела со сложностью в папку Infrastructure файл класса по имени нием, приведенным в листинге Листинг 14. 17. MVC, ContentMiddleware. cs добавим с определе­ 14.17. Con ten tмiddleware. cs Infrastructure Содержимое файла из папки using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ConfiguringApps.Infrastructure class ContentMiddleware { private RequestDelegate nextDelegate; puЫic puЬlic ContentMiddleware(RequestDelegate next) => nextDelegate = next; async Task Invoke(HttpContext httpContext) { if (httpContext. Request. Path. ToString (). ToLower () == "/middleware") puЫic await httpContext.Response.WriteAsync( "This is from the content middleware", Encoding.UTFB); else { await nextDelegate.Invoke(httpContext); Компоненты промежуточного ПО не реализуют какой-либо интерфейс и не явля­ ются унаследованными от какого-то общего базового класса. Взамен они определяют 398 Часть 11. Подробные сведения об инфраструктуре ASP.NEТ Core MVC 2 конструктор. принимающий объект RequestDelegate. и метод Invoke (). Объект RequestDelegate представляет следующий компонент промежуточного ПО в цепоч­ ке, а метод Invoke () вызывается. когда ASP.NET получает НТТР-запрос. Информация о запросе и ответе НТТР. которая будет возвращена клиенту, предо­ ставляется методу Invoke () через аргумент свойства рассматриваются в главе 17. HttpContext. Класс HttpContext и его Invoke () в послан на URL вида а пока достаточно знать. что метод 14.17 инспектирует НТТР-запрос и проверяет. был ли он /middleware. Если это так. тогда клиенту отправляется простой текстовый ответ: если использовался другой URL, то запрос перенаправляется следующему компоненту листинге в цепочке. Конвейер запросов настраивается внутри метода Configure () класса Startup. 14.18 из примера приложения удалены методы MVC и применен класс ContentMiddleware в качестве единственного компонента в конвейере. В листинге Листинг 14. 18. Использование специального компонента промежуточного ПО в файле using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; using ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseМiddleware<Contentмiddleware>(); Специальные компоненты расширяющего метода UseMiddleware () промежуточного UseMiddleware () ПО регистрируются внутри метода с помощью Configure (). Метод применяет параметр типа для указания класса промежуточного ПО. Именно так инфраструктура ASP.NET Core может построить список всех компо­ нентов промежуточного ПО. которые собирается использовать. и затем создать их эк­ земпляры для формирования цепочки. На рис. жения и запроса URL вида 14.6 показан результат запуска прило­ /middleware. На рис. ем класса 14.7 изображен конвейер промежуточного ПО. созданный с применени­ ContentMiddleware. Когда инфраструктура ASP.NET Core получает НТТР­ запрос, она передает его единственному компоненту промежуточного ПО, зарегис­ трированному в классе Startup. Если URL нерирует результат, который возвращается является /middleware, ASP.NET Core то компонент ге­ и отправляется клиенту. Глава 14. Конфигурирование прил оже ний С ф 399 localhost 65416/niiddfeware This is from the content middleware l_ Рис. 14.6. Генерация содержимого из специального компонента промежуточного ПО Промежуточное ПО для генерации содержимого r- ...,.. ш z а.: (/) 1 s :r: 1 "' Q_ 1 ~ Рис. URL Ответ .J <t: Если -----1 14. 7. отличается от '- ------ <111(------ о Пример конвейера промежуточного ПО /midd l eware , тогда класс Con te ntMidd leware переда ­ ет запрос следующему компоненту в цепочке. Поскольку других компонентов нет. за­ прос достигает ограничительного обработчика. предоставленного инфраструктурой ASP.NET Core при создании конвейера. который посылает запрос обратно через кон­ вейер в противоположном направлении (после того , как вы ознакомитесь с работой других типов промежуточного ПО, данный процесс обретет больший смысл). Использование служб в промежуточном ПО Применять служб ы. настроенные в методе ко контроллеры. Инфраструктура Confi gureSe rv ices ASP.NET Core () ,могут не толь­ инспектирует конструкторы клас­ сов промежуточного ПО и и спользует службы для предоставления значений любым аргументам. которые были определены. В листинге Cont entMiddl e ware димости предоставления объекта Листинг 14.19 добавлен аргумент. который сообщает к конструктору класса ASP.NET Core UptimeService . 14.19. Применение службы в файле Contentмiddleware. cs из файла Infrastructure using System . Te x t; usin g System.T hrea ding.Ta s ks ; u si ng Micr o s oft . AspNetCore.Http; names pace Co nfi guringApps .In fra st ructure class Conte ntM i ddleware { private RequestDel egate nextDelegate ; puЫic о необхо­ Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 400 UptiшeService private uptime; puЬlic Contentмiddleware(RequestDelegate nextDelegate uptiшe = next, UptiшeService up) ( next; = up; async Task Invoke (Htt pContext htt pCont ext) ( if (httpContext.Re que s t.Pat h .ToS trin g () . To Lowe r () puЫic " /mi dd l e war e " ) { await httpC ontext.Re s po nse.WriteAsyn c ( "This is froш the content шiddleware "+ $" (uptime: (uptime.Uptime}ms)", Encoding.UTFB); else { await ne xt Delegate.Invo ke (httpContext ) ; Наличие возможности использования служб озн а чает, что компоненты проме­ жуточного ПО могут разделять общую функциональность и избегать дублирования кода. Запуск приложения и запрос да. представленного на рис. URL вида / mi dd l e ware 14.8. D loc4!host6541 б/mtddle С приводит к п олучению выво ­ Х ф loca1host 65416"1111ddle~vare This is from the content middleware (uptime: 281ms) Рис. 14.8. Применение службы в специальном промежуточном ПО Создание промежуточного ПО для обхода Следующий тип промежуто чного ПО перехватывает запросы до того. как они до ­ стигнут компонентов для генерации содержимого , чтобы обойти процесс прохожде­ ния конвейера. часто в целях. связанных с производительностью . В л истинге приведено содержимое файла ю~асса по имени ленного в папку Листинг Sho rtCi rc ui tMiddlewa re . c s, Inf r astru cture . 14.20. Содержимое файла ShortCircui tмiddleware . cs из папки Infrastructure using System.Linq ; us ing System.Threading.Tasks; us ing Mic ros o ft. Asp NetCore.Htt p ; na mes pa ce Co nf igu r in gAp ps.Inf rast ru c ture pu Ы i c c l ass Shor t Ci rc uitMiddleware { 14.20 добав­ Глава 14. Конфигурирование приложений 401 private RequestDelegate nextDelegate; ShortCircuitMiddleware(RequestDelegate next) => nextDelegate = next; puЬlic async Task Invoke(HttpContext httpContext) if (httpContext.Request.Headers["User-Agent"] .Any(h => h.ToLower() .Contains("edge"))) httpContext.Response.StatusCode = 403; else { await nextDelegate.Invoke(httpContext); puЫic Этот компонент промежуточного ПО проверяет заголовок User-Agent запроса. который браузеры применяют для идентификации самих себя. Использование заго­ ловка User-Agent для опознания специфических браузеров не вполне надежно для реализации в реальном приложении, но достаточно для рассматриваемого примера. Понятие мобход" применяется из-за того, что такой тип промежуточного ПО не всегда направляет запросы следующему компоненту в цепочке. В данном случае. если заголовок User-Agent содержит элемент edge, 4 03 - ForЬidden (403 - запрещено) состояния в тогда компонент устанавливает код и не направляет запрос следующему компоненту. Так как запрос отклонен, нет никакого смысла давать возможность за­ просу обрабатываться остальными компонентами, что привело бы к ненужному пот­ реблению системных ресурсов. Взамен обработка запроса прекращается раньше, а клиенту посылается ответ 403. Компоненты промежуточного ПО получают запросы в порядке. в котором они были настроены в классе Startup, что означает необходимость настройки промежу­ точного ПО для обхода перед промежуточным ПО для генерации содержимого (лис­ тинг 14.21). Листинr 14.21. Реrистрация компонента промежуточноrо ПО для обхода в файле using using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; ConfiguringApps.Infrastructure; namespace Conf iguringApps { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЫic Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 402 puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) ( app.UseМiddleware<ShortCircuitмiddleware>(); app.UseMiddleware<ContentMiddleware>(); В результате запуска приложения и запрашивания любого браузера нент Microsoft Ed~e появится ошибка ShortCircui tMiddleware с использованием игнорирует и передает следующему компоненту в цепочке. т.е. ответ сгенерируется, когда запрошенным рис. URI, Запросы из других браузеров компо­ 403. 14.9 иллюстрируется добавление будет URL /middleware. На в конвейер компонента промежуточного ПО для обхода. Промежуточное ПО Промежуточное ПО для генерации для обхода 1-- . ш ------ r z Рис. 14.9. ------- r 1 а.: (J) <( содержимого 1 Ответ 1- .J "' 1 1 ---- 1 Ответ 1 1 ------!-< .J ' 1 с5 Добавление в конвейер компонента промежуточного ПО для обхода Соэдание промежуточного по для редактирования запросов Следующий тип компонента промежуточного ПО. который мы исследуем, ответ не генерирует. Взамен компонент изменяет запросы перед тем. как они достигнут других компонентов, находящихся дальше в цепочке. Такой вид промежуточного ПО приме­ няется преимущественно в целях интеграции с платформой, чтобы обогатить пред­ ставление ASP.NET Core запроса НТТР средствами, специфическими для платформы. Он также может использоваться при подготовке запросов, чтобы их легче было об­ рабатывать последующими компонентами. В качестве демонстрации добавим в пап­ ку Infrastructure файл BrowserTypeMiddleware. cs промежуточного ПО, приведенным в листинге Листинг с определением компонента 14.22. 14.22. Содержимое файла BrowserТypeМiddleware. cs из папки Infrastructure using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ConfiguringApps.Infrastructure Глава 14. Конфигурирование nриложений 403 class BrowserTypeMiddleware private RequestDelegate nextDelegate; puЫic puЫic BrowserTypeMiddleware(RequestDelegate next) => nextDelegate = next; async Task Invoke(HttpContext httpContext) httpContext.Items["EdgeBrowser"] = httpContext.Request.Headers["User-Agent"J .Any(v => v.ToLower() .Contains("edge") ); await nextDelegate.Invoke(httpContext); puЫic Новый компонент инспектирует заголовок edge, что предполагает HttpContext предоставляет мент User-Agent запроса и ищет в нем эле­ возможность выдачи запроса браузером через свойство Items Edge. Объект словарь, который применяется для передачи данных между компонентами, и результат поиска в заголовке сохраня­ ется с ключом EdgeBrowser. Чтобы продемонстрировать, как компоненты промежуточного ПО могут взаимо­ действовать друг с другом, в листинге 14.23 показан класс ShortCircui tMiddleware, который отклоняет запросы, когда они поступают от браузера ние на основе сгенерированных компонентом Листинг 14.23. Edge, принимая реше­ BrowserTypeMiddleware данных. Взаимодействие с другим компонентом в файле ShortCircui tмiddleware. cs из папки Infrastructure using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ConfiguringApps.Infrastructure class ShortCircuitMiddleware { private RequestDelegate nextDelegate; puЫic puЫic puЫic ShortCircuitMiddleware(RequestDelegate next) => nextDelegate = next; async Task Invoke(HttpContext httpContext) { if (httpContext. Iteш.s [ "EdgeBrowser"] as bool? == true) { httpContext.Response.StatusCode = 403; else { await nextDelegate.Invoke(httpContext); Из-за присущей им природы компоненты промежуточного ПО, редактирующие запросы, необходимо помещать перед компонентами, с которыми они взаимодейс­ твуют или которые полагаются на вносимые ими изменения. В листинге BrowserTypeMiddleware 14.24 класс зарегистрирован как первый компонент в конвейере. Часть 11. Подробные сведения об инфраструктуре ASP. NET Core MVC 2 404 Листинг Регистрация компонента промежуточного ПО для редактирования 14.24. запросов в файле using using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps ( puЫic class Startup ( puЬlic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЫic void Configure ( IApplicationB11ilder арр, IHostingEnvironment env) ( app.UseМiddleware<BrowserTypeМiddleware>(); app.UseMiddleware<ShortCircuitMiddleware>(); app.UseMiddleware<ContentMiddleware>(); Размещение компонента в начале конвейера гарантирует. что до попадания в дру­ гие компоненты запрос уже будет модифицирован (рис. Промежуточное ПО для редактирования запросов 14.1 О). Промежуточное ПО Промежуточное ПО для обхода для генерации содержимого 1-ш 1 z 1 Запрос 1 - - - - -- 1 а.: сп <( 1 , Рис. 14. 1О. - - - 1 1 Ответ Ответ 1 ' -· r 1 1 - - - - ·- 1 ' ]~ ---·--"-.:.•-- - ' -· .. 1 - ... rc -· - - :_' 1 ~: l :~ 1 ' с ••1 /:.~ j -·------- Добавление в конвейер компонента промежуточного ПО для редактирования запросов Создание промежуточного по для редактирования ответов Последний тип промежуточного ПО оперирует на ответах. генерируемых другими компонентами в конвейере. Это полезно для регистрации в журнале деталей запросов и их ответов или для обработки ошибок. В листинге файла ErrorMiddleware. cs. 14.25 представлено содержимое который добавлен в папку монстрации данного вида компонента промежуточного ПО. Infrastructure для де­ Глава 14. Конфигурирование приложений Листинг 14.25. 405 ErrorMiddleware. cs Infrastructure Содержимое файла из папки using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace ConfiguringApps.Iпfrastructure puЫic class ErrorMiddleware { private RequestDelegate пextDelegate; puЬlic ErrorMiddleware(RequestDelegate next) { пextDelegate puЬlic аsупс await = пехt; Task Iпvoke(HttpCoпtext httpContext) пextDelegate.Iпvoke(httpCoпtext); if (httpContext.Response.StatusCode == 403) { await httpContext.Response .WriteAsync("Edge not supported", Encoding.UTF8); else if (httpContext.Response.StatusCode == 404) { await httpContext.Response .WriteAsync("No content middleware response", Encoding.UTF8); Компонент не заинтересован в запросе до тех пор. пока он не пройдет свой путь через конвейер промежуточного ПО и не будет сгенерирован ответ. Если кодом состо­ яния ответа является 403 или 404. тогда компонент добавляет к ответу описательное сообщение. Все другие ответы игнорируются. В листинге гистрируется в классе 14.26 класс компонента ре­ Startup. Совет. У вас может возникнуть вопрос, откуда поступил код состояния (404 - 4 04 - Not Found не найдено), поскольку он не устанавливается ни одним из трех созданных ком­ понентов промежуточного ПО. Дело в том, что именно так инфраструктура ASP.NEТ кон­ фигурирует ответ, когда запрос входит в конвейер, и этот ответ будет тем результатом, который возвращается клиенту, если компоненты промежуточного ПО его не изменяли. Листинг 14.26. Регистрация компонента промежуточного ПО ДllЯ редактирования ответов в файле using using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threadiпg.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 406 n a mespace Co nfiguringApps { puЫic class Startup { puЫic void Con figureSer v i ces ( IService Co ll ec t i on se rv i ces ) { services.AddSingleton<Upt imeS ervice> (); services.AddMvc(); puЬli c void Co nfigure(IAppli cat i o nBuilde r арр , IHost ing Env i r onment env) { app.UseМiddleware<ErrorMiddleware>(); app.UseMiddleware<BrowserT ypeMiddleware > () ; app.UseMiddl eware<ShortCir c u itMiddleware> () ; app.UseMiddl e ware<ContentMiddleware> (); Класс ErrorMiddleware зарегистрирован в первой позиции конвейера. что может выглядеть странным для компонента , который заинтересован только в ответах. но регистрация компонента в начале цепочки обеспечивает ему возможность инспекти ­ ровать ответы, сгенерированные любыми другими компонентами (рис. 14. 11). В слу­ чае помещения компонента дальше в конвейере он сможет инспектировать ответы. генерируе мые только компонентами. находящимися по сле него. ПромеJ1Суточное ПО Промежут очное П О для редактирования для редакти ро вания Промежуточ но е П О заnросов для обхода ответов 1-- ___. ш z о..: (f) ----------- <( Рис. 14.11. для ген ерации содержимо го г Заnрос Ответ 1 Отвы 1 - 1 ···- 1· · 1 +--- ---j Промежуто чно е П О 1 - -- 1 О т ве т 1 1 <( Добавление в конвейер компонента промежуточного ПО для редактирования ответов Эффект от нового пром ежуточного ПО можно увидеть, запустив прилож е ние и запросив любой URL кроме /middle wa re. 14.12. Результатом будет сообщение об ошибке . показанное на рис. D loc~ ostб541б/oth~rVtf Х С Ф localhost·65416/ott1e1 Url content middleware response Рис. 14. 12. Редактирование ответов от других компонентов промежуточного ПО Глава 14. Конфигурирование приложений Особенности вызова метода Платформа ASP.NET Core 407 Conf iqure () исследует метод Configure () перед его вызовом и по­ лучает список его аргументов, которые он предоставляет с использованием служб, настроенных в методе в табл. ConfigureServices ().или специальных служб. описанных 14.5. Таблица 14.5. Специальные службы, доступные как аргументы метода Confiqure () Описание Тип IApplicationBuilder Этот интерфейс определяет функциональность, требующуюся для настройки конвейера промежуточного ПО приложения Этот интерфейс определяет функциональность, необходимую IHostingEnvironment для различения типов сред, таких как среда разработки и про­ изводственная среда IAppl.ica tionВuil.der Использование интерфейса Хотя определять любые аргументы для метода большинстве классов Startup IApplicationBuilder, потому Conf igure () не обязательно, в будет применяться, по меньшей мере, интерфейс что он позволяет создать конвейер промежуточно­ го ПО, как демонстрировалось ранее в главе. В случае специальных компонентов промежуточного ПО для регистрации классов используется расширяющий метод UseMiddleware ().Сложные пакеты промежуточного ПО для генерации содержимого предоставляют единственный метод. который настраивает все их компоненты проме­ жуточного ПО за один шаг - точно так же, как они предоставляют единственный ме­ тод для определения применяемых ими служб. Для МVС доступны два расширяющих метода. описанные в табл. Таблица 14.6. 14.6. Расширяющие методы IApplicationBuilder для МУС Описание Имя UseMvcWithDefaultRoute() Этот метод настраивает компоненты промежуточного ПО для UseMvc () MVC с использованием стандартного маршрута Этот метод настраивает компоненты промежуточного ПО для MVC с применением специальной конфигурации мар­ шрутизации, указанной посредством лямбда-выражения Маршрутизация - это процесс, посредством которого URL запросов сопоставля­ ются с контроллерами и действиями, определенными в приложении; маршрутиза­ ция подробно рассматривается в главах 15 и 16. полезен при изучении разработки приложений ний будет вызываться метод UseMvc (). Метод UseMvcWi thDefaul tRoute () MVC. но в большинстве приложе­ даже если в результате явно определя­ ется точно такая же конфигурация маршрутизации. как и создаваемая методом UseMvcWi thDefaul tRoute (). В листинге 14.27 приведен пример. Такой подход де­ лает конфигурацию маршрутизации. используемую в приложении. очевидной для других разработчиков и облегчает дальнейшее добавление новых маршрутов (что в какой-то момент требуется практически во всех приложениях). 408 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Листинг 14.27. Настройка компонентов промежуточного ПО для МУС в файле using using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { void ConfigureServices(IServiceCollection services} { services.AddSingleton<UptimeService>(}; services.AddMvc(}; puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env} app.UseMiddleware<ErrorMiddleware>(); app.UseMiddleware<BrowserTypeMiddleware>(}; app.UseMiddleware<ShortCircuitMiddleware>(}; app.UseMiddleware<ContentMiddleware>(); puЫic app.UseМvc(routes => routes.МapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }) ; Поскольку настраивает компоненты промежуточного ПО для генерации со­ MVC держимого, метод UseMvc {) вызывается после регистрации всех других компонен­ тов промежуточного ПО. Чтобы подготовить службы, от которых зависит AddMvc () должен вызываться в методе Использование интерфейса Интерфейс MVC. метод ConfigureServices (). IHostingEnvironment IHostingEnvirorunent предоставляет базовую - но важную - инфор­ мацию о среде размещения, в которой функционирует приложение, с применением свойств, описанных в табл. Таблица 14. 7. Свойства 14.7. IHos tingEnvironmen t Имя Описание ApplicationName Это свойство возвращает имя приложения, установленное размещающей платформой EnvirorunentName Это свойство возвращает строку, которая описывает текущую среду, как объясняется далее в главе 409 Глава 14. Конфигурирование приложений Окончание табл . 14.7 Имя Описание Co пtentRootPa t h Это свойство возвращает путь , по которому находятся файлы содержимого и конфигурации приложения Это свойство возвращает строку, указывающую каталог, в ко ­ WebRoot Path тором находится статическое содержимое для приложения . Обычно им будет папка Соп t.е пt Root Fi l e Prov ider wwwroo t Это свойство возвращает объект, который реализует ин­ тepфeйc Micr oso ft. AspNetCore. Fi l e Providers. I Fil ePro v ider и может использоваться для чтения файлов из папки , указанной свойством Conten tRootPath Это свойство возвращает объект, который реализует ин­ WebRoo t Fil e Pro v ider Micro s o ft. AspNe t Co re . File Pr o v i de r s. IFi l e Prov i der и может применяться для чтен и я файлов папки, указанной свойством WebRootPath терфейс Свойства Conten tRoo tPath и Web RootPath из интерес ны. но в большинстве при­ ложений не нужны. потому что имеется встроенный компонент пром ежуточного ПО . который можно использовать для доставки статического содержимого , ка к описан о в разделе " Включение статич еского соде ржимого·· дал ее в гл аве. Важным свойством является Enviro nmen tName , которое позволяет модифициро­ вать конфигурацию приложения на основе среды. где оно выполняется. По соглаше­ нию различают три среды (среда разработки да (St aging) и производственная среда (Deve l o pmen t). (Produc t ion)). подготовительная сре­ Текущая среда размещения устанавливается с применением пе р ем е нной среды по имени ASPNETCORE_ENVIRONMEN T. Чтобы установить п ерем енную с реды. выбе ­ ConfiguringApps Properties (Свойств а Co пf i g u rin gApp s ) и з меню Project (Проект) в Visual Studio и в открывшемся диалоговом окне перейдем на вкладку Debug (Отладка). Дважды щелкнем н а поле Value (Значение) для переменной среды . рем пункт по умолчанию установленной в De ve lopme nt. и изменим ее на St agi ng (рис. Сохраним изменения, чтобы новое имя среды вступило в силу. МфЦ llSEx!>reos нsex.r." L"""""URL Erмrorment VамЬ.еs Nan-.e дSPNE1COREJ: ~~ /1RONME>• Slogr.g ,..... . . . . ..". ., ,. , . . ."..._. . _.,.._.",....._. .",."........ ......""_,. r.,,. \\ltl>S........~ Рис. 14. 1З . ~" Установка имени среды размещения 14.13). Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 410 Совет. Имена сред не чувствительны к регистру символов, так что трактуются как одна и та же среда. Хотя Staging и staging Development, Staging и Production явля­ ются общепринятыми именами сред, допускается использовать любое желаемое имя. Это может быть полезно, например, когда над проектом трудятся несколько разработчиков, каждый из которых требует разных конфигурационных настроек. В разделе "Работа со сложными конфигурациями" далее в главе объясняется, как учитывать сложные различия между конфигурациями сред. Выяснить внутри метода можно через свойство Configure (), какая среда размещения применяется. IHostingEnvironment. EnvironmentName или путем вызо­ ва одного из расширяющих методов, которые оперируют с объектами реализации IHostingEnvironment Таблица 14.8. и описаны в табл. Расширяющие методы 14.8. IHostingEnvironment Имя Описание IsDevelopment () Этот метод возвращает является IsStaging () Этот метод возвращает является IsProduction () IsEnvironment(env) если именем среды размещения true, если именем среды размещения true, если именем среды размещения true, если имя среды размещения сов­ Staging Этот метод возвращает является true, Development Production Этот метод возвращает падает со значением аргумента env Расширяющие методы применяются для изменения набора компонентов про­ межуточного ПО в конвейере, чтобы приспособить поведение приложения к раз­ личным средам размещения. В листинге 14.28 с помощью одного из расширяю­ щих методов гарантируется, что специальные компоненты промежуточного ПО. созданные ранее в главе, присутствуют только в среде размещения Листинг 14.28. Использование среды размещения в файле из папки using using using using using using using using using Development. Startup. cs ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); Глава 14. Конфигурирование приложений puЫic void Configure(IApplicationBuilder 411 IHostingEnvirorunent env) { арр, if (env.IsDevelopment()) ( app.UseМiddleware<ErrorМiddleware>(); app.UseМiddleware<BrowserТypeМiddleware>(); app.UseМiddleware<ShortCircuitМiddleware>(); app.UseМiddleware<Contentмiddleware>(); app.UseMvc(routes => routes.MapRoute( name: "default", template: "{controller=Home)/{action=Index)/{id?)"); }) ; Специальные компоненты промежуточного ПО не добавляются в конвейер при текущей конфигурации, в которой переменная среда была установлена в В результате запуска приложения и запроса ошибка 4 04 - Not Found, вида /middleware Staging. будет получена т.к. доступными компонентами промежуточного ПО явля­ ются лишь те. которые настроены методом для обработки указанного URL UseMvc (), а они не имеют контроллера URL. На заметку! Протестировав влияние от изменения среды размещения, не забудьте изменить ее снова на Development, иначе оставшиеся примеры в главе не будут работать долж­ ным образом. Добавление оставшихся компонентов промежуточного программного обеспечения Существует набор наиболее часто используемых компонентов промежуточного ПО, которые полезны в большинстве проектов МVС и применяются в примерах, рассмат­ риваемых в настоящей книге. По мере добавления таких компонентов к конвейеру запросов в последующих разделах будет объясняться их работа. Обеспечение возможности обработки исключений Даже самое тщательно написанное приложение будет сталкиваться с исключения­ ми, поэтому важно их надлежащим образом обрабатывать. В листинге трируется добавление к конвейеру запросов в классе Startup 14.29 демонс­ компонентов промежу­ точного ПО, которые имеют дело с исключениями. Кроме того, удалены специальные компоненты промежуточного ПО. чтобы можно было сосредоточиться на МVС. Листинг 14.29. Добавление компонентов промежуточного ПО для обработки исключений в файле using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 412 using using using using using Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps ( puЫic class Startup ( puЫic void ConfigureServices(IServiceCollection services) ( services.AddSingleton<UptimeService>(); services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) ( if (env.IsDevelopment()) ( app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); else { app.UseExceptionHandler("/Home/Error"); app.UseMvc(routes => routes.MapRoute( name: "default", template: "(controller=Home)/(action=Index)/(id?}"); }) ; Метод IJseStatusCodePages () добавляет к ответам, не имеющим содержимого, 4 04 - Not Found (404- не найдено), что может быть описательные сообщения вроде удобно, т.к. не все браузеры отображают пользователю собственные сообщения. Метод IJseDeveloperExceptionPage () настраивает компонент промежуточного ПО для обработки ошибок, который отображает детали исключения в ответе, в том числе связанную трассировочную информацию. Информация подобного рода не долж­ на быть видимой пользователям, поэтому вызов IJseDeveloperExceptionPage () де­ лается только в среде разработки, которая определяется с помощью объекта реализа­ ции IHostingEnvironmment. В подготовительной или производственной среде взамен используется метод IJseExceptionHandler (). Он настраивает обработку ошибок, позволяющую отобра­ жать специальное сообщение об ошибке, которое не раскрывает особенности внут­ ренней работы приложения. Аргументом метода URL, IJseExceptionHandler () является на который должен быть перенаправлен клиент, чтобы получить сообщение об ошибке. Это может быть любой URL, предоставляемый приложением. но по соглаше­ /Home/Error. В листинге 14.30 добавлена возможность генерации исключений по требованию действия Index контроллера Home и определено действие Error, так что запросы, генерируемые IJseExceptionHandler (), могут быть обработаны. нию применяется Глава 14. Конфигурирование прил ожений Листинг 14.30. 413 Генерация и обработка исключений в файле HomeController. cs из папки Controllers using System. Col le ct ions.Gen eric; using Micr osoft . Asp NetCore.Mvc ; using Configur i ngApps.Infrastru c t u r e; n amespace ConfiguringApps . Controllers p uЫic class HomeCont rol l er : Cont r ol l er private UptimeService uptime; puЫic HomeCon tr oller(UptimeService up ) => up t ime = up ; puЬlic ViewResult Index(bool throwException = false) if (throwException) { throw new System . NullReferenceException() ; return View(new Dictionary<string, string> [ "Message"] = "This is the Index action", ["Uptime"] = $"{uptime.Uptime}ms " }} ; ViewResul t Error (} => View (nameof (Index} , new Dictionary<string, string> { [ "Message"] = "This is the Error action " }) ; puЬlic I ndex, с помощью средства привязки моделей . 26. обеспечивают получение значения thr owExcepti o n из запроса. Действие генерирует исключение Nul lRe feren ceExcept i on , если значение thr owExcepti o n равно t ru e. и выполня ется нормально, если знач е ние throw Ex cepti o n равно false. Действи е Er r or использует пр едставле ние Inde x для отображения простого с ооб­ щения. Запустив приложение и запросив URL вида / Home/I ndex?throwException =tr ue. можно просмотреть результаты функционирования разных компонен­ Изменения. внесенные в действие которое обсуждается в главе тов промежуточного ПО для обработки исключений . Строка запроса предоставля­ ет значение для аргумента действия In dex. от имени среды размещения. На рис. 14. 14 а наблюдае мый ответ будет зависеть показан вывод . порожда ем ый методом Us e De ve l ope rEx c ep t i onPage () (дл я среды размещения Devel opment) UseE x ceptionHand ler () (для всех остальных сред размещения) . АГ' u11r1;зr dled е ceotюn oc... LJrred 'll11k • .... +- Рис. 14.14. с ф - .t) ' ~ n "(> и методом х ! Обработка исключений в среде разработки и подготовительно й/ производственной среде 414 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Страница исключения для разработчика предлагает детали исключения, а также возможность исследовать его информацию трассировки стека и запрос, который при­ вел к исключению. В противоположность этому страница исключения для пользова­ теля должна применяться просто для указания о том, что что-то пошло не так. Включение средства Средство Browser Link Browser Link (Ссылка на браузер). описанное в главе 6, предназначено для управления браузерами на стадии разработки. Серверная часть средства Link Browser реализована в виде компонента промежуточного ПО, который должен быть до­ бавлен в классе Startup как часть конфигурации приложения, потому что без тако­ го компонента интеграция с Visual Studio работать не будет. Средство Browser Link полезно только на стадии разработки и не должно использоваться в подготовитель­ ной или производственной среде, поскольку оно модифицирует ответы, генерируемые другими компонентами промежуточного ПО, с целью вставки кода JavaScript, кото­ рый открывает НТГР-подключения с серверной стороной, чтобы она могла получать уведомления о перезагрузке страницы. В листинге тод UseBrowserLink ко для среды размещения Листинг 14.31. 14.31 показано, как вызывать ме­ (),который регистрирует компонент промежуточного ПО толь­ Development. Включение средства из папки Browser Link в файле Startup.cs ConfiguringApps using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Dependencyinjection; using ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); void Configure(IApplicationBuilder арр, IHostingEnvironment env) { if (env.IsDevelopment()) ( app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseBrowserLink(); else { app.UseExceptionHandler("/Home/Error"); puЫic app.UseMvc(routes => routes.MapRoute( name: "defaul t", template: "{controller=Home}/{action=Index}/{id?}"); )) ; Глава 14. Конфигурирование nриложений 415 Включение статического содержимого Финальный компонент промежуточного ПО, полезный для большинства проектов, предоставляет доступ к файлам в папке чать изображения, файлы wwwroot, что позволяет приложениям вклю­ JavaScrlpt и таблицы стилей CSS. Метод UseSta ticFiles () добавляет компонент, который обходит конвейер запросов для статических файлов 14.32). (листинг Листинг 14.32. Включение статического содержимого в файле из папки using using using using using using using using using Startup. cs ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЫic puЫic void Configure ( IApplicationBuilder арр, IHostingEnvironment env) { if (env.IsDevelopment()) ( app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseBrowserLink(); else ( app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home)/{action=Index}/{id?}"); }) ; Статическое содержимое обычно требуется вне зависимости от среды размеще­ ния.из-за чего метод что элемент link UseStaticFiles () узеру загрузить таблицу стилей деть результат, представленный вызывается для всех сред. Это означает, Index будет надлежаще работать и позволит бра­ CSS из Bootstrap. Запустив приложение. можно уви­ на рис. 14. 15. в представлении 416 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 D Resu1t С х ф localhos 65416 Message This is the lndex action Uptime 12ms L_ _ Рис. 14.15. Включение статического содержимого Конфигурирование приложения Некоторые данные конфигурации часто изменяются. например. когда приложение переносится из среды разработки в производственную среду и потому для серверов баз данных требуются друтие параметры. Вместо того чтобы жестко кодировать по­ добную информацию в классе Startup. платформа ASP.NET Core разрешает постав ­ лять данные конфигурации из целого диапазона более легко изменяемых источников. таких как переменные среды , аргументы командной строки и файлы в формате (JavaScгipt Object Notatioп - система обозначений для объектов JSON JavaScript). Данные конфигурации обычно обрабатываются автоматически. но из-за того. что мы заменили стандартные настройки в классе Program. понадобится явно добавить код, который будет получать данные и делать их доступными остальной части прило­ жения (листинг Листинг using using using using using using using using using using 14.33). 14.33. Загрузка данных конфигурации в файле Program. cs из папки ConfiguringApps System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps ( puЫic class Program ( puЬlic static void Main (string [) args) ( BuildWebHost(args) .Run(); puЫic static IWebHost BuildWebHost(string[) args) ( return new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory() ) Глава 14. Конфигурирование nриложений 417 .ConfigureAppConfiguration((hostingContext, config) => { config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); config.AddEnvironшentVariaЫes(); if (args != null) { confiq.AddC01111DandLine(arqs); } }) .UseIISintegration() .UseStartup<Startup>() . Build (); Метод ConfigureAppConfiguration () применяется для обработки данных кон­ WebHost.BuilderContext и объект, ре­ IConfigurationBuilder. В классе WebBostBuilderContext описанные в табл. 14.9. фигурации: его аргументами являются объект ализующий интерфейс определены свойства, Таблица 14.9. Свойства, определенные в классе WeЬВostвuilderContext Описание Ими HostingEnvirorunent Это свойство возвращает объект, который реализует интер­ фейс IHostingEnvirorunent и предоставляет информа­ цию о среде размещения, где функционирует приложение. Подробные сведения были даны в разделе "Использование интерфейса Configuration IHostingEnvirorunent" ранее в главе Это свойство возвращает объект, который реализует интер­ фейс IConfiguration, предоставляющий доступ только по чтению к данным конфигурации в приложении Интерфейс IConfigurationBuilder применяется при подготовке данных кон­ фигурации для остальной части приложения, которая обычно выполняется с исполь­ зованием расширяющих методов. Три метода. задействованные в листинге добавления данных конфигурации, описаны в табл. Таблица 14.1 О. Расширяющие методы интерфейса 14.33 для 14.10. IConfigura tionBui lder, предназначенные для добавления данных конфигурации Ими Описание AddJsonFile () Этот метод применяется для загрузки данных конфигура­ ции из файла AddEnvironrnentVariaЫes () JSON, такого как appset tings. j son Этот метод используется для загрузки данных конфигура­ ции из переменных среды AddCoпunandLine() Этот метод применяется для загрузки данных конфигура­ ции из аргументов командной строки, указываемых при запуске приложения 418 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Из трех методов, которые использовались для загрузки данных конфигурации 14.33, самым интересным считается AddJsonFile ().В аргументах ме­ AddJsonFile () указываются имя файла, является ли файл необязательным, и в листинге тода должны ли данные конфигурации загружаться повторно, если файл изменяется: config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); В значениях аргументов указан файл по имени appsettings. j son, ставляет собой общепринятое имя для конфигурационного файла которое пред­ JSON. Файл необя­ зателен, т.е. если он не существует, то никакого исключения не генерируется, и он будет отслеживаться на предмет изменений, так что данные конфигурации могут ав­ томатически обновляться. Повторная загрузка данных конфигурации Система конфигурации ASP.NET Core поддерживает повторную загрузку данных, когда кон­ фигурационные файлы изменяются. Некоторые встроенные компоненты промежуточного ПО, такие как система регистрации в журнале, поддерживают такую функциональность, что означает возможность изменения уровней регистрации во время выполнения без необхо­ димости в перезапуске приложения. Аналогичные средства можно также предусмотреть при разработке специальных компонентов промежуточного ПО. Однако просто тот факт, что средство делает что-то возможным, вовсе не означает, что оно является целесообразным. Внесение изменений в файлы конфигурации производственных систем - верный способ вызвать простой из-за отказа. Слишком легко допустить ошибку при модификации и получить неправильно работающую конфигурацию. Даже при успешном изменении могут возникать непредвиденные последствия, такие как переполнение дисков журнальными данными или резкое снижение производительности. Рекомендуется избегать динамического редактирования и удостоверяться, что все измене­ ния прошли через стандартные процедуры разработки и тестирования в подготовительной среде, прежде чем развертывать их в производственной среде. Может возникнуть соблазн заняться модификацией активной системы, чтобы установить причину проблемы, но это редко заканчивается хорошо. Если вы обнаруживаете, что занимаетесь редактированием файлов конфигурации производственной системы, то должны задаться вопросом, готовы ли вы превратить небольшую проблему в гораздо более крупную. Создание конфигурационного файла JSON Самое распространенное применение файла а рр s е t t i ngs . j s оn - хранение строк подключения к базам данных и настроек регистрации в журналах, но в нем можно держать любые данные, в которых нуждается приложение. Чтобы посмотреть, как работает система конфигурации. добавим в корневой ката­ лог проекта новый файл денным в листинге Листинг 14.34. JSON по имени appsettings. j son с содержимым, приве­ 14.34. Содержимое файла appsettings. json из папки Confi9urin9Apps "ShortCircui tMiddleware": { "EnaЫeBrowserShortCircuit": true Глава 14. Конфигурирование приложений Формат JSON 419 делает возможной структуру. предназначенную для настроек кон­ JSON в листинге 14.34 определяет категорию конфигура­ ShortCircui tMiddleware, содержащую свойство по имени EnaЬleBrowserShortCircui t, которое установлено в true. фигурации. Содержимое ции под названием Формат JSON: кавычки и запятые Если ранее вы не имели дела с форматом полезно уделить некоторое время чтению спецификации на веб-сайте форматом JSON, тогда www. j son. org. С JSON легко работать, к тому же большинство платформ предлагают хорошую поддержку для генерации и разбора данных JSON, в числе которых и приложения MVC (примеры ищите в главах уровне клиента используется простой АРl-интерфейс JavaScript. 20 и 21 ), а на В действительности боль­ шая часть разработчиков приложений MVC вообще не будут взаимодействовать с JSON на­ JSON вручную требуется только в файлах конфигурации. прямую, и написание кода Существуют две ловушки, в которые попадают многие разработчики, не имеющие опыта работы с JSON. Хотя вы по-прежнему должны найти время на чтение спецификации, знание наиболее распространенных проблем предоставит определенную отправную точку в ситуа­ ции, когда Visual Studio или ASP.NET Соге не смогут провести разбор ваших файлов JSON. global. j son, в котором Ниже показано добавление к стандартному содержимому файла присутствуют две самые распространенные проблемы: { "ShortCircuitMiddleware": 1 "EnaЫeBrowserShortCircuit": true mysetting : [ fast, slow ] Во-первых, практически все в JSON помещается в кавычки. Очень легко забыть, что вы пи­ шете вовсе не код С#, и посчитать, что имена свойств и значения должны быть восприняты без кавычек. Тем не менее, в JSON все кроме булевских значений и чисел должно поме­ щаться в кавычки: "ShortCircuitMiddleware": "EnaЫeBrowserShortCircuit": true "mysetting": [ "fast", "slow"] Во-вторых, при добавлении нового свойства к JSОN-описанию объекта вы должны помнить о необходимости добавления запятой после предшествующего символа фигурной скобки: "ShortCircuitMiddleware": { "EnaЫeBrowserShortCircuit": ) true , "mysetting": [ "fast", "slow"] Заметить отличие довольно трудно даже когда оно выделено полужирным (именно потому такая ошибка является распространенной), но здесь помещена запятая после символа закрывающего раздел ShortCircui tMiddleware. ), Однако будьте внимательны, потому что замыкающая запятая, после которой отсутствует раздел, также приводит к ошибке. Если внесенные вами изменения в файл JSON вызвали проблемы, то в первую очередь нужно выполнить проверку на предмет двух указанных выше ошибок. Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 420 Использование данных конфигурации Класс Startup может получить доступ к данным конфигурации путем опреде­ ления конструктора с аргументом типа IConfiguration. Когда в классе Program UseStartup (),данные конфигурации, подготовленные методом ConfigureAppConfiguration, применяются для создания объекта Startup. В лис­ тинге 14.35 демонстрируется добавление к классу Startup конструктора и доступ к вызывается метод данным конфигурации. Листинг 14.35. Получение и использование данных конфиrурации в файле using using using using using using using using using using Startup. cs из папки ConfiguringApps System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; Microsoft.Extensions.Configuration; namespace ConfiguringApps { puЫic class Startup { puЬlic Startup(IConfiguration configuration) Configuration = configuration; IConfiguration Configuration { get; ) void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЬlic puЫic puЫic if void Configure(IApplicationBuilder арр, IHostingEnvironment env) ((Configuration.GetSection("ShortCircuitмiddleware")? .GetValue<bool>("EnaЫeBrowserShortCircuit")) .Value) app.UseMiddleware<BrowserTypeMiddleware>(); app.UseMiddleware<ShortCircuitMiddleware>(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage{); app.UseStatusCodePages(); app.UseBrowserLink(); else { app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home)/{action=Index)/{id?)"); )) ; Глава 14. Конфигурирование приложений Конструктор получает объект реализации свойству по имени Configuration, IConfiguration 421 и присваивает его которое затем может применяться для доступа к данным конфигурации, загруженным из переменных среды, командной строки и файла appsettings. j son. Чтобы получить значение, понадобится пройти по структуре данных до требуемо­ го раздела конфигурации, представленного с помощью еще одного объекта, который IConfiguration, предоставляющий IConfigurationRoot членов (табл. 14. l l). реализует интерфейс для Таблица 14.11. Члены, определяемые интерфейсом подмножество доступных IConfiqura tion Описание Имя [key] Этот индексатор используется для получения строкового зна­ чения, соответствующего указанному ключу GetSection(name) Этот метод возвращает объект реализации key IConfiguration, который представляет раздел данных конфигурации GetChildren () Этот метод возвращает перечисление объектов реализации IConfiguration, которые представляют подразделы текуще­ го объекта конфигурации 14.12, можно применять для опери­ IConfiguration с целью получения значений и их Расширяющие методы, описанные в табл. рования объектами реализации преобразования из строк в другие типы. Таблица 14.12. Расширяющие методы для интерфейса IConfiqura tion Имя Описание GetValue<T>(keyName) Этот метод получает значение, ассоциированное с указанным ключом GetValue<T>(keyName, defaultValue) keyName, и пытается преобразовать его в тип т Этот метод получает значение, ассоциированное с указанным ключом keyName, и пытается преобразовать его в тип т. Если значение для ключа в данных конфигурации отсутствует, тогда будет использоваться стандартное значение Важно не предполагать, В листинге 14.35 defaul tValue что конфигурационное значение будет указано. применяется n u 11-условная операция, чтобы удостоверить­ ShortCircui tMiddleware до того, как пытаться из­ EnaЬleBrowserShortCircui t. В результате специальный компо­ ся в получении раздела влечь значение нент промежуточного ПО добавится к конвейеру запросов, только если значение EnaЬleBrowserShortCircui t из раздела лено и установлено в ShortCircui tMiddleware было опреде­ true. Конфигурирование регистрации в журнале Платформа ASP.NET Core включает поддержку сбора и обработки журнальных дан­ ных. а многие встроенные компоненты промежуточного ПО написаны так, чтобы ге­ нерировать журнальные сообщения. В большинстве проектов регистрация в журнале настраивается автоматически. но поскольку в классе Program используются индиви­ дуальные конфигурационные операторы, для настройки средства регистрации в жур­ нале необходимо добавить операторы. показанные в листинге 14.36. Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 422 Листинг 14.36. Конфигурирование регистрации в журнале в файле из папки using using using using using using using using using using Program. cs ConfiguringApps System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps { puЫic class Program ( static void Main(string[] args) { BuildWebHost(args) .Run(); puЫic puЫic static IWebHost BuildWebHost(string[] args) { return new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration( (hostingContext, config) => { config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); config.AddEnvironmentVariaЫes(); if (args != null) { config.AddCommandLine(args); } ) ) .ConfigureL099in9((hostin9Context, 1099in9) => { 1099in9.AddConfiguration( hostin9Context.Configuration.GetSection("Lo99in9")); 1099in9.AddConsole(); 1099in9.AddDebu9(); }) .UseIISintegration() .UseStartup<Startup>() .Build(); Метод ConfigureLogging () настраивает систему регистрации в журнале с при­ менением лямбда-функции, которая получает объект WebHostBuilderContext (упо­ ILoggingBuilder. Для мянутый ранее в главе) и объект, реализующий интерфейс интерфейса ILoggingBuilder предусмотрен набор расширяющих методов. позволя­ ющих конфигурировать систему регистрации в журнале (табл. 14.13). Глава 14. Конфигурирование приложений Таблица Расширяющие методы дпя интерфейса 14.13. 423 ILoggingBuilder Описание Имя Этот метод используется для конфигурирования системы регис­ AddConfiguration () трации в журнале с применением данных конфигурации, которые были загружены из файла appsettings. j son, из командной строки либо из переменных среды Этот метод отправляет журнальные сообщения консоли, что AddConsole () удобно в случае запуска приложения с использованием команды dotnet run Этот метод отправляет журнальные сообщения в окно AddDebug ( ) (Вывод), когда выполняется отладчик Output Visual Studio Этот метод отправляет журнальные сообщения в журнал событий AddEventLog () Windows, что удобно в ситуации, когда приложение ASP.NET Core MVC развернуто на сервере Windows и нужно, чтобы сообщения из него смешивались с сообщениями от приложений других типов Данные конфигурации системы регистрации в журнале Метод AddConfiguration () применяется для конфигурирования системы регис­ трации в журнале, используя данные конфигурации, которые обычно определены в файле 14.37 в файл appsettings. j son добавляется Logging, которое соответствует имени, применяемо­ AddConfiguration () из листинга 14.36. appsettings. j son. В листинге раздел конфигурации по имени му для метода Листинг 14.37. Добавление раздела конфигурации в файле appsettings. json из папки ConfiguringApps "ShortCircuitMiddleware": ( } , "EnaЫeBrowserShortCircuit": true "Logging": { "Lo9Level 11 : "Default": 11 DeЬu9 11 , "System": "Information", "Мicrosoft": "Information" В конфигурации системы регистрации в журнале указан уровень сообщений, ко­ торые должны отображаться из различных источников журнальных данных. Система регистрации в журнале поддерживает шесть уровней отладочной информации, кото­ рые описаны в табл. Элемент 14.14 Defaul t нальных сообщений в Debug в порядке их важности. в листинге Debug, 14.37 устанавливает порог для отображения жур­ т.е. отображаться будут только сообщения с уровнем и выше. Остальные элементы переопределяют стандартные настройки для журнальных сообщений из специфических пространств имен, чтобы журнальные со­ общения, которые поступают из пространств имен лись только в случае, если они относятся к уровню System и Microsoft, Information и выше. отобража­ 424 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Таблица 14.14. Уровни отладочной информации ASP.NEТ Уровень Описание Trace Этот уровень используется для сообщений, которые полезны во время разработки, но не требуются в производственной среде Debug Этот уровень применяется для детализированных сообщений, требующих­ ся разработчикам при отладке с целью выявления проблем Information Этот уровень используется для сообщений, которые описывают общее функционирование приложения Warning Этот уровень применяется для сообщений, описывающих события, кото­ рые не предвиделись, но и не приводят к прерыванию работы приложения Error Этот уровень используется для сообщений, которые описывают ошибки, прерывающие работу приложения Critical Этот уровень применяется для сообщений, описывающих катастрофичес­ кие отказы None Этот уровень используется для отключения регистрации журнальных сообщений Для просмотра результатов включения регистрации в журнале запустим при­ ложение с применением отладчика Debugging Visual Studio, выбрав пункт меню {ОтладкаQЗапустить отладку). Заглянув в окно Output, DebugqStart можно увидеть жур­ нальные сообщения. которые показывают. как обрабатывался каждый НТГР-запрос: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[l] Request starting HTTP/l.l GET http://localhost:65417/ info: Microsoft.AspNetCore.Mvc.Internal.ControllerActioninvoker[l] Executing action method ConfiguringApps.Controllers.HomeController.Index (ConfiguringApps) with arguments (False) - ModelState is Valid info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[l] Executing ViewResult, running view at path /Views/Home/Index.cshtml. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActioninvoker[2] Executed action ConfiguringApps.Controllers.HomeController.Index (ConfiguringApps) in 1597.ЗSЗSms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 1695.6314ms 200 text/html; charset=utf-8 Создание специальных журнальных сообщений Журнальные сообщения в предыдущем разделе генерировались компонента­ ми ASP.NET Core и MVC, которые обрабатывают НТТР-запрос и производят ответ. Сообщения подобного рода способны предоставлять полезную информацию, но мож­ но также генерировать специальные журнальные сообщения, которые являются спе­ цифичными для конкретного приложения, как демонстрируется в листинге Листинг 14.38. Специальная регистрация в журнале в файле из папки using using using using Controllers System.Collections.Generic; Microsoft.AspNetCore.Mvc; ConfiguringApps.Infrastructure; Microsoft.Extensions.Logging; 14.38. HomeController. cs Глава 14. Конфигурирование nриложений 425 namespace ConfiguringApps.Controllers { puЫic class HomeController : Controller private OptimeService uptime; private ILogger<HomeController> logger; puЫic HomeController(UptimeService up, ILogger<HomeController> log) uptime logger puЫic up; log; ViewResult Index(bool throwException = false) { {Request.Path} at uptime {uptime.Uptime}"); logger.LogDeЬug($"Handled if (throwException) { throw new System.NullReferenceException(); return View(new Dictionary<string, string> ["Message"] = "This is the Index action", ["Optime"] = $"{uptime.Optimelms" 1); puЫic ViewResul t Error () => View (nameof ( Index), new Dictionary<string, string> { [ "Message"] = "This is the Error action" 1); Интерфейс ILogger определяет функциональность, требуюIЦУюся для созда­ ния журнальных записей и получения объекта, который реализует этот интер­ фейс. а класс HomeController имеет ILogger<HomeController>. Параметр конструктор, принимающий аргумент типа типа позволяет системе регистрации в жур­ нале использовать имя класса в журнальных сообщениях, и значение для аргумента конструктора предоставляется автоматически через средство внедрения зависимос­ тей. которое будет описано в главе 18. Располагая объектом реализации ILogger, журнальные сообщения можно со­ здавать с применением расширяющих методов, определенных в пространстве имен Microsoft. Extensions. Logging. Предусмотрены методы для каждого уровня от­ 14.14. Класс HomeCon trol ler использует метод LogDebug () для создания сообщения на уровне Debug. Чтобы увидеть результат, по­ ладочной информации из табл. надобится запустить приложение с применением отладчика в окне Output Visual Studio и поискать журнальное сообщение вроде такого: dbug: ConfiguringApps.Controllers.HomeController[O] Handled / at uptime 12 При запуске приложения отображается много сообщений. что может затруднить выявление отдельных сообщений. Одиночные сообщения легче найти, если щелкнуть на кнопке Clear All (Очистить все) в верхней части окна Output и затем перезагрузить страницу в браузере. что обеспечит отображение журнальных сообщений, относя­ щихся к единственному запросу. 426 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 Конфигурирование внедрения зависимостей Стандартная конфигурация для приложений ASP.NET Саге включает подготовку поставщика служб, который используется средством внедрения зависимостей, де­ тально описанным в главе фигурирования в класс Листинг using using using using using using using using using using 14.39. 18. В листинге 14.39 показано добавление операторов кон­ Program. Конфигурирование служб в файле Program.cs из папки ConfiguringApps System; System.Collections.Generic; System. IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace Conf iguringApps { puЬlic class Program { static void Main ( string [] args) { BuildWebHost(args) .Run(); puЫic static IWebHost BuildWebHost(string[] args) { return new WebHostBuilder() . UseKestrel () .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration( (hostingContext, config) => { config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); puЫic config.AddEnvironmentVariaЬles(); i f (args != null) { config.AddCommandLine(args); ) }) .ConfigureLogging( (hostingContext, logging) => { logging.AddConfiguration( hostingContext.Configuration.GetSection("Logging" )); logging.AddConsole(); logging.AddDebug(); )) .UseIISintegration() .UseDefaultServiceProvider((context, options) => options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build (); Глава 14. Конфигурирование nриложений Метод UseDefaultServiceProvider () ASP.NET Core. 427 применяет встроенный поставщик служб Хотя доступны альтернативные поставщики служб, в большинстве про­ ектов подойдут встроенные средства, а использовать сторонний компонент рекомен­ дуется только при решении специфической задачи и наличии хорошего понимания концепции внедрения зависимостей. которая будет обсуждаться в главе UseDefaul tServiceProvider () объекты WebHostBuilderContext Метод лучает 18. принимает лямбда-функцию, которая по­ и ServiceProviderOptions, применяе­ мые для конфигурирования встроенного поставщика служб. Единственное свойство конфигурации называется тельной при работе с ValidateScopes, Entity Framework Core, Конфигурирование служб Когда метод са Startup, AddMvc () и его установка в false как объяснялось в главе является обяза­ 8. MVC вызывается внутри метода ConfigureServices () клас­ он настраивает все службы, которые требуются приложениям Преимущество такого подхода связано с удобством, поскольку все службы MVC MVC. регис­ трируются за один шаг, но если необходимо изменить стандартное поведение, тогда понадобится выполнить дополнительную работу по реконфигурированию служб. Метод AddMvc () инфраструктура возвращает объект, реализующий интерфейс MVC IMvcBuilder, а предлагает набор расширяющих методов, предназначенных для расширенного конфигурирования, наиболее полезные из которых описаны в табл. 14.15. Многие методы конфигурирования связаны со средствами. рассматрива­ емыми в последующих главах. Таблица 14.15. Полезные расширяющие методы IMvcBuilder Описание Имя AddМvcOptions () Этот метод конфигурирует службы, используемые инфра­ структурой AddFormatterMappings() MVC, как объясняется ниже Этот метод применяется для конфигурирования средства, которое позволяет клиентам указывать формат получае­ мых данных (глава AddJsonOptions () 20) Этот метод используется для конфигурирования способа создания данных AddRazorOptions() JSON (глава 20) Этот метод применяется для конфигурирования механиз­ ма визуализации AddViewOptions () Razor (глава 21) Этот метод используется для конфигурирования способа обработки представлений инфраструктурой MVC, в том числе применяемые механизмы визуализации (глава Метод AddMvcOptions () конфигурирует самые важные службы МVС. Он прини­ мает функцию, получаюшую объект MvcOptions, который предоставляет набор кон­ фигурационных свойств: наиболее полезные свойства приведены в табл. Перечисленные в табл. 21) 14.16 методы 14.16. конфигурирования используются для точ­ ной настройки способа функционирования инфраструктуры MVC, и в указанных главах вы найдете подробные описания связанных с ними средств. Тем не менее, в качестве небольшой демонстрации в листинге нять метод AddMvcOpt.ions () 14.40 показано, как можно приме­ для изменения параметра конфигурации. 428 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 Таблица 14.16. Избранные свойства MvcOptions Имя Описание Conventions Это свойство возвращает список соглашений модели, которые используются для настройки способа создания контроллеров и действий инфраструктурой Filters MVC (глава 31) Это свойство возвращает список глобальных фильтров (глава FormatterMappings 19) Это свойство возвращает сопоставления, применяемые для того, чтобы позволить клиентам указывать формат получаемых ими данных (глава InputFormatters 20) Это свойство возвращает список объектов, используе­ мых для разбора данных запросов (глава ModelBinders 20) Это свойство возвращает список связывателей модели, которые применяются для разбора запросов (глава ModelValidatorProviders Это свойство возвращает список объектов, используе­ мых для проверки достоверности данных (глава OutputFormatters 27) Это свойство возвращает список классов, которые фор­ матируют данные, отправляемые из контроллеров (глава RespectBrowserAcceptHeader Это свойство указывает, должен ли учитываться заголо­ вок 14.40. Accept, когда принимается решение о том, какой Изменение параметра конфигурации в файле из папки API 20) формат данных задействовать для ответа (глава Листинг 26) 20) Startup. cs ConfiguringApps void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); puЫic services.Adc!Мvc() .AddМvcOptions(options => { options.RespectBrowserAcceptHeader = true; }) ; Лямбда-выражение, передаваемое методу ект MvcOptions, который AcceptHeader в true. Это AddMvcOptions (). принимает объ­ RespectBrowser используется для установки свойства изменение позволяет клиентам оказывать большее влия­ ние на формат данных. выбираемый процессом согласования содержимого. как будет объясняться в главе 20. Работа со сложными конфигурациями Если вы нуждаетесь в поддержке большого количества сред размещения или меж­ ду средами размещения имеется много отличий, тогда применение операторов для ветвления конфигураций в классе Startup i f может в результате дать конфигура­ цию. которую трудно читать и редактировать, не вызывая неожиданных изменений. Глава 14. Конфигурирование приложений 429 В последующих разделах будут описаны разнообразные способы использования клас­ са Startup для сложных конфигураций. Создание разных внешних конфигурационных файлов Стандартная конфигурация для приложения, обеспечиваемая классом ищет конфигурационные файлы JSON, ния. где запускается приложение. Таким образом, файл по имени production. j son JSON Листинг в класс 14.41. Prograrn, 14.41 восстановлен оператор. который загружает удаленный в начале главы. Загрузка файлов среды в файле из папки using using using using using using using using using using appsettings. можно применять для хранения настроек. специфичных для про­ изводственной среды. В листинге файл Prograrn. которые специфичны к среде размеще­ Program. cs ConfiguringApps System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps { puЫic class Program { static void Main ( striпg [] args) { BuildWebHost(args) .Run(); р11Ыiс puЫic static IWebHost BuildWebHost(string[] args) { return new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration( (hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName} .json", optional: true, reloadOnChange: true); config.AddEnvironmentVariaЫes(); if (args != null) { config.AddCommandLine(args); 1 )) .ConfigureLogging( (hostingContext, logging) => { logging.AddConfiguration( hostingContext.Configuration.GetSection("Logging ")); logging.AddConsole(); logging.AddDebug(); }) .UseIISintegration() 430 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 .UseDefaultServiceProvider( (context, options) => options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); ) ) .UseStartup<Startup>() .Build(); При загрузке данных конфигурации из файла. специфичного для платформы, кон­ фигурационные настройки. которые содержатся в нем. переопределяют любые сущес­ твующие данные с теми же самыми именами. Например. в листинге содержимое файла по имени нением шаблона элемента appsettings. development. j son. ASP.NET Coпfiguratioп File. приведено Данные конфигурации в этом файле устанавливают значение EnaЬleBrowserShortCi Совет. Может показаться, что файл 14.42 созданного с приме­ rcui t в false. appset tings. development. j son исчезает сразу же appsettings. j son в окне после его создания. Щелкнув на стрелке слева от записи Solutioп Explorer, легко увидеть, что Visual Studio группирует вместе элементы с похо­ жими именами. Листинг 14.42. Содержимое файла appsettings. development. j son из папки ConfiguringApps "ShortCircuitMiddleware": { "EnaЬleBrowserShortCircuit": false Файл appsettings. j son загрузится при запуске приложения. а за ним файл appset tings. developmen t. j son, если приложение выполняется в среде разработ­ ки. В результате свойство EnaЬleBrowserShort.Circui t получает значение false, когда приложение функционирует в среде разработки, и true - когда в подготови­ тельной или производственной среде. Создание разных методов конфигурирования Выбор разных файлов с конфигурационными данными может быть удобен, но не предоставлять полного решения для сложных конфигураций. т.к. файлы данных не содержат операторы С#. Если желательно варьировать операторы конфигурирования, используемые для создания служб или регистрации компонентов промежуточного ПО. тогда допускается применять разные методы, причем имя метода должно вклю­ чать название среды размещения (листинг Листинг 14.43). 14.43. Использование разных имен методов в файле Startup. cs из папки ConfiguringApps using System; using System.Collections.Generic; Глава 14. Конфигурирование приложений using using using using using using using using 431 System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; ConfiguringApps.Infrastructure; Microsoft.Extensions.Configuration; namespace ConfiguringApps { puЫic class Startup { Startup(IConfiguration configuration) Configuration = configuration; puЫic puЫic IConfiguration Configuration { get; ) void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc() .AddMvcOptions(options => options.RespectBrowserAcceptHeader = true; puЫic ) ) ; void ConfigureDevelopmentServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); puЫic services.AddМvc(); void Configure(IApplica.tionВuilder арр, IHostingEnviroruaent env) { app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseМvc(routes => { routes.MapRoute( name : "defaul t" , template: "{controller=Home}/{action=Index}/{id?}"); puЬlic }) ; void ConfigureDevelopment(IApplicationBuilder IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseBrowserLink(); app.UseStaticFiles(); puЬlic арр, app.UseМvcWithDefaultRoute(); Когда инфраструктура Configure () в классе ASP.NET Core Startup, ищет методы ConfigureService () и она сначала выясняет, имеются ли методы, имена которых включают название среды размещения. В листинге 14.43 был добавлен ме­ ConfigureDeveloprnentServices (),который в среде Developrnent будет при­ меняться вместо метода ConfigureServices (),и метод ConfigureDeveloprnent (), который в среде Developrnent будет использоваться вместо метода Configure (). тод 432 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Можно определить отдельные методы для каждой среды, которую необходимо подде­ рживать, и полагаться на стандартные методы. вызываемые в случае недоступности методов. специфичных для той или иной среды. В рассматриваемом примере это оз­ начает. что для подготовительной и производственной сред будут применяться мето­ ды ConfigureServices () и Configure (). Внимание! Если определены методы, специфичные для среды, то стандартные мето­ ды не вызываются. Например, в листинге 14.43 инфраструктура ASP.NET Core не бу­ Configure () в среде Development, потому что имеется метод ConfigureDevelopment ().Другими словами, каждый метод несет ответственность за дет вызывать метод полную конфигурацию, требуемую для среды, на которую он ориентирован. Создание разных классов конфигурирования Использование разных методов означает. что вы не обязаны применять операто­ ры для проверки имени среды размещения, но в результате могут быть получе­ i f ны крупные классы, которые сами по себе являются проблемой. В случае особенно сложных конфигураций последним усовершенствованием будет создание собствен­ ного конфигурационного класса для каждой среды размещения. Когда инфраструк­ тура ASP.NET Core ищет класс Startup, она первым делом проверяет, есть ли класс с именем, включающим название текущей среды размещения. С этой целью добавим в проект файл класса по имени зано в листинге Листинг 14.44. содержимое которого пока­ Содержимое файла из папки using using using using StartupDevelopment. cs, 14.44. StartupDevelopment. cs ConfiguringApps Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Dependencylnjection; ConfiguringApps.Infrastructure; namespace ConfiguringApps { puЫic class StartupDevelopment { puЬlic void ConfigureServices(IServiceCollection services) { services.AddSingleton<UptimeService>(); services.AddMvc(); puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseBrowserLink(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); Класс StartupDevelopment содержит методы ConfigureServices () и Configure (),которые специфичны для среды размещения Development. Чтобы Глава 14. Конфигурирование nриложений 433 возможность найти класс спе­ предоставить инфраструктуре ASP.NEТ Core цифичный для среды. в класс потребуется внести изменение, показанное в листинге Листинг Startup, 14.45. 14.45. Startup, специфичного для среды, Program. cs из папки ConfiguringApps Включение класса в файле using using using using using using using using using using Program System; System.Collections.Generic; System.IO; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore; Microsoft.AspNetCore.Hosting; Microsoft.Extensions.Configuration; Microsoft.Extensions.Logging; System.Reflection; namespace ConfiguringApps puЫic class Program { puЫic static void Main(string[] args) { BuildWebHost(args) .Run(); static IWebHost BuildWebHost(string[] args) { return new WebHostBuilder() . UseKestrel () .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureAppConfiguration( (hostingContext, config) => { var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName) .json", optional: true, reloadOnChange: true); puЫic config.AddEnvironmentVariaЬles(); i f (args !=null) { config.AddCommandLine(args); ) )) .ConfigureLogging( (hostingContext, logging) => { logging.AddConfiguration( hostingContext.Configuration.GetSection("Logging ")); logging.AddConsole(); logging.AddDebug(); }) .UseIISintegration() .UseDefaultServiceProvider( (context, options) => options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); )) .Usestartup(nameof(ConfiguringApps)) .Build(); 434 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Вместо указания какого-то 1U1acca методу UseStartup () передается имя сборки, которая должна использоваться. Когда приложение запускается. инфраструктура ASP.NET Core 1U1acc, имя которого включает название среды размеще­ StartupDevelopment или StartupProduction, и возвращаться к класса Startup, если специфичный для среды 1U1acc отсутствует. будет искать ния, такой как применению Резюме В главе рассматривались способы конфигурирования приложений сана роль 1U1ассов Startup и Program, MVC. Была опи­ а также стандартных настроек конфигурации, которые они обеспечивают. Кроме того, обсуждалась обработка запросов с примене­ нием конвейера и использование различных типов промежуточного ПО для управле­ ния потоком запросов и результирующих ответов. В следующей главе будет представ­ лена система маршрутизации, посредством которой инфраструктура МVС имеет дело с отображением URL запросов на контроллеры и действия. ГЛАВА 15 Маршрутизация URL ASP.NET предполагалось наличие прямой связи между запра­ в ранних версиях URL шиваемыми и файлами на жестком диске сервера. Работа сервера заключа­ лась в получении запроса от браузера и доставке вывода из соответствующего файла. Подобный подход успешно работал для инфраструктуры страница ASPX Web Fonns, в которой каждая представляла собой и файл, и самодостаточный ответ на запрос. Это совершенно не имеет смысла в приложении MVC, где запросы обрабатывают­ ся методами действий из классов контроллеров, и однозначное соответствие между запросами и дисковыми файлами отсутствует. При обработке URL в MVC платформа ASP.NET применяет систему маршрутиза­ ASP.NET Core. В настоящей главе вы ции, которая была модернизирована для версии узнаете, как использовать систему маршрутизации для обеспечения мощной и гибкой обработки URL в своих проектах. Вы увидите, что система маршрутизации позволяет создавать любой желаемый шаблон URL и выражать его в ясной и лаконичной мане­ ре. Система маршрутизации выполняет две функции. • Исследование входящих URL и выбор контроллера и действия для обработки запроса. • Генерация исходящих URL. Такие URL встречаются в НТМL-разметке. визуали­ зированной из представлений. так что специфическое действие может быть инициировано, когда пользователь щелкает на ссылке (после чего ссылка снова становится входящим URL). В главе внимание будет сосредоточено на определении маршрутов и их примене­ нии для обработки входящих URL, чтобы пользователь мог достичь контроллеров и действий. Есть два способа создания маршрутов в приложении MVC: маршрутиза­ ция на основе соглашений и маршрутизаt~ия с помощью атрибутов. Мы рассмотрим здесь оба подхода. Затем в следующей главе будет показано, как использовать те же самые маршру­ ты для генерации исходящих URL, которые необходимо включать в представления, а также объяснены способы настройки системы маршрутизации и продемонстрирова­ но применение связанного средства под названием области. В табл. сводка. позволяющая поместить маршрутизацию в контекст. В табл. 15.2 представлена сводка по главе. 15. 1 приведена 436 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Таблица 15.1. Помещение маршрутизации в контекст Вопрос Ответ Что это такое? Система маршрутизации несет ответственность за обработку входящих запросов, а также выбор кон­ троллеров и методов действий для их обработки. Вдобавок система маршрутизации используется для генерации маршрутов в представлениях, известных как исходящие URL Система маршрутизации обеспечивает гибкую обра­ Чем она полезна? ботку запросов без привязки сов в проекте Как она используется? URL к структуре клас­ Visual Studio Сопоставление URL с контроллерами и методами действий определяется в файле путем применения атрибута Startup. cs или Rou te к контроллерам Существуют ли какие-то скрытые Конфигурация маршрутизации для сложного прило­ ловушки или ограничения? жения может стать трудной в управлении Существуют ли альтернативы? Нет. Система маршрутизации - неотъемлемая часть ASP.NET Core Таблица 15.2. Сводка по главе Задача Решение Листинг Определите маршрут 15.9 Возможность не указывать сегменты Определите стандартные значения URL для сегментов маршрута 15.1015.12 Отображение между URL и методами действий Сопоставление сегментов URL, кото­ Определите статические сегменты 15.1315.16 Определите специальные переменные 15.1715.19 рые не имеют соответствующих пере­ менных маршрутизации Передача сегментов URL методам действий сегментов Возможность не указывать сегменты Определите необязательные сегменты 15.20, 15.21 Используйте сегмент общего захвата 15.22, 15.23 Применяйте ограничения маршрута 15.2415.33 Определение маршрута внутри Используйте маршрутизацию с помощью контроллера атрибутов 15.3415.38 URL, для которых не предусмотрены стандартные значения Определение маршрутов, которые соответствуют любому количеству сегментов URL Ограничение URL, которым может соответствовать маршрут Глава 15. Маршрутизация URL 437 Подготовка проекта для примера Empty (Пустой) по имени ASP.NET Core Web Application (.NET Core) Для целей главы мы создадим новый проект типа UrlsAndRoutes (Веб-приложение с применением шаблона ASP.NET Core (.NET Core)). показанные в листинге 15. 1, Startup операторы. MVC Framework. страниц Добавим в класс чтобы обеспечить поддержку ошибок для разработчиков и статических файлов. Листинг 15.1. Конфигурирование приложения в файле из папки using using using using using using using using Startup. cs UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); puЬlic арр, IHostingEnvironment env) { app.UseМvc(); Создание класса модели Все усилия в текущей главе сконцентрированы на сопоставлении URL запросов с действиями. Единственному имеющемуся классу модели передаются детали о конт­ роллере и методе действия. которые были выбраны для обработки запроса. Создадим папку Models и добавим в нее файл класса по имени веденным в листинге Листинг 15.2. Resul t. cs 15.2. Содержимое файла Resul t. cs из папки Models using System.Collections.Generic; namespace UrlsAndRoutes.Models { puЬlic class Result { puЫic string Controller { get; set; } puЫic striпg Action { get; set; } puЫic IDictionary<string, object> Data { get; } = new Dictionary<striпg, object>(); с определением. при­ 438 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Свойства Controller и Action будут использоваться для указания того, как за­ Data - для хранения других деталей о запросе, выда­ прос был обработан, а словарь ваемых системой маршрутизации. Создание примеров контроллеров Для демонстрации работы маршрутизации нам нужны простые контроллеры. Создадим папку cs Controllers и добавим в нее файл класса по имени с содержимым из листинга HomeController. 15.3. Листинг 15.З. Содержимое файла HomeController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class HomeController : Controller ViewResult Index() => View("Result", new Result { Controller = nameof(HomeController), Action = nameof(Index) puЬlic )) ; Метод действия определенный в контроллере Home, вызывает метод Resul t (которое определяет­ ся в следующем разделе) и передает экземпляр Resul t в качестве объекта модели. Свойства объекта модели устанавливаются с применением операции nameof; они Index (), для визуализации представления по имени View () будут использоваться для указания, какие контроллер и метод действия бьmи задейс­ твованы для обслуживания запроса. Аналогичным образом CustomerContr·oller. cs Листинг 15.4. добавим в папку Со с определением контроллера Содержимое файла namespace UrlsAndRoutes.Controllers class CustomerController : Controller ViewResul t Index () => View ( "Resul t", new Result { Controller = nameof(CustomerController), Action = nameof(Index) puЫic ) ) ; ViewResult List() => View("Result", new Result { Controller = nameof(CustomerController), Action = nameof(List) puЫic ) ) ; о 11 е r s файл класса Customer (листинг 15.4). CustomerController.cs using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; puЫic nt r из папки Controllers Глава 15. Маршрутизация URL Третий (и последний) контро;шер определяется в файле по имени AdminCon tro l (листинг внутри папки Controllers Листинг Содержимое файла 15.5. 15.5). 439 ler. cs Он следует тому же подходу. как и ранее. AdminController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class AdminController : Controller ViewResult Index() => View("Result", new Result { Controller = nameof(AdminController), Action = nameof(Index) puЬlic }) ; Создание представления Во всех методах действий, определенных в предыдущем разделе, указано представ­ ление Resul t. что позволяет создать одно представление, которое будет разделяться всеми контроллерами. Создадим папку нее новый файл представления по имени в листинге Листинг 15.6. Views/Shared и добавим в Resul t. cshtml с содержимым, показанным Содержимое файла Result.cshtml из папки 15.6. Views/Shared @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="m-1 р-1"> <tаЫе class="taЫe taЬle-bordered taЬle-striped taЫe-sm"> <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) 1 <tr><th>@key :</th><td>@Model.Data[key]</td></tr> </tаЫе> </body> </html> Bootstrap таблицу, которая Bootstrap в проект, создадим файл bower. j son, используя шаблон элемента Bower Configuration File (Файл конфи­ гурации Bower). и укажем пакет Bootstrap в разделе dependencies (листинг 15.7). Представление содержит стилизованную с помощью отображает свойства объекта модели. Чтобы добавить 440 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Листинг 15. 7. Добавление пакета Bootstrap в файле Ьower. j из папки son UrlsAndRoutes "name": "asp.net ", "private": true , "dependen cie s": { "bootstrap": "4. О. 0-alpha. 6" Финальный подготовительный шаг предусматривает создание в папке файла_ Viewimports. cshtml, гательные функции дескрипторов для применения в представлениях тируется пространство имен модели (листинг Листинг V i ew s внутри которого настраиваются встроенные вспомо­ Razor и импор­ 15.8). 15.8. Содержимое файла_Viewimports. cshtml из папки Views @using UrlsAndRoutes.Models @addTagHelpe r *, Microsoft.AspNetCore.Mvc.TagHelpers Конфигурация в классе того. как инфраструктура Startup MVC не содержит никаких инструкций относительно должна сопоставлять НТТР-запросы с контроллерами и действиями. После запуска приложения любой запрашиваемый результате ответ 404 - Not Found (404 - С l0<~ikost5"1839 С ф URL будет давать 15.1. в не найдено). как видно на рис . Х localhos ~~ ,9 Status Code: 404; Not Found Рис. 15.1. Введение в шаблоны Запуск примера приложения URL Система маршрутизации работает с использованием множества маршрутов . Вместе такие маршруты образуют схему бой набор URL. URL для приложения. представляющую со­ которые приложение будет распознавать и реагировать на них. Набирать вручную все индивидуальные URL, которые планируется поддерживать URL. с кото­ URL соответствует шаблону. тогда обработки этого URL. Начнем с про­ в приложении. не придется . Взамен каждый маршрут содержит шаблон рым сравниваются входящие URL. Если входящий он применя ется системой маршрутизации для стого URL: http://mysite.com/Adrnin/Index URL могут быть разбиты на сегменты - части URL кроме имени хоста и строки / . В приведенном выше примере запроса , которые отделяются друг от друга символом URL есть два сегмента. как показано на рис. 15.2 . Глава 15. Маршрутизация URL 441 http://mysite.com/Adm in/Index t t Рис. Первый Второй сегмент сегмент Сегменты в примере 15.2. Первый сегмент содержит слово Admin, а второй - URL слово Index. В глазах чело­ века совершенно очевидно, что первый сегмент имеет отношение к контроллеру, а второй - к действию. Однако. естественно, такое отношение должно быть выражено URL, который будет в состоянии воспринимать URL. соответствующий примеру URL: с использованием шаблона система маршрутизации. Вот шаблон {controllerf/{action} При обработке входящего НТТР-запроса задача системы маршрутизации заключа­ ется в сопоставлении запрошенного URL с шаблоном и извлечении из URL значений для переменных сегментов, определенных в шаблоне. Переменные сегментов выражаются с применением фигурных скобок (символов { }). и В приведенном выше примере шаблона присутствуют две переменные сег­ ментов с именами controller будет Приложение controller и action, так что Admin, а значением переменной MVC значением переменной сегмента сегмента action - Index. обычно будет иметь несколько маршрутов, и система маршру­ тизации будет сравнивать входящий URL с шаблоном URL каждого маршрута до тех URL будет соответствовать пор, пока не найдет совпадение. По умолчанию шаблон любому URL, который имеет подходящее количество сегментов. Например, шаблон {controller} / { action} как описано в табл. будет соответствовать любому с двумя сегментами, URL 15.3. Таблица 15.З. Сопоставление URL URL запроса Переменные сегментов http://mysite.com/Admi.n/I ndex controller=Admin,action=I ndex h t tp: / /mys i te. com/ Admi.n Соответствия нет - ht tp: / /mysi te. com/Admin/Index/Soccer Соответствия нет - сегментов слишком много В табл. • 15.3 Шаблоны сегментов слишком мало отражены две ключевые линии поведения шаблонов URL URL. консервативны в отношении количества сопоставляемых сегмен­ тов. Совпадение будет происходить только для тех URL, которые имеют то же самое количество сегментов. что и шаблон. Это можно наблюдать во втором и третьем примерах в таблице. • Шаблоны тов. Если URL либеральны в отношении содержимого сопоставляемых сегмен­ URL имеет правильное количество сегментов, то шаблон извлечет зна­ чение каждого сегмента для переменной сегмента, каким бы оно ни было. Описанные две стандартные линии поведения являются ключом к пониманию функционирования шаблонов URL. стандартные линии поведения. Позже в главе будет показано, как изменить такие 442 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 Создание и регистрация простого маршрута Имеющийся шаблон URL можно Маршруты определяются в файле методу UseMvc (), В листинге 15.9 использовать для Startup. cs определения маршрута. и передаются в качестве аргументов который применяется для настройки MVC в методе Configure (). показан базовый маршрут. отображающий запросы на контроллеры в примере приложения. Листинг 15.9. Определение базового маршрута в файле из папки using using using using using using using using Startup. cs UrlsAnd.Routes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.DependencyinJection; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЬlic puЬlic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); арр. UseМvc (routes => { routes.МapRoute(name: "default", template: "{controller}/{action}"); }) ; Маршруты создаются с использованием лямбда-выражения, передаваемого в виде аргумента конфигурационному методу ект. который реализует интерфейс UseMvc ().Лямбда-выражение получает объ­ IRouteBuilder из пространства имен Microsoft. AspNetCore. Routing, а маршруты определяются с применением расширяющего ме­ тода MapRoute (). Чтобы облегчить понимание маршрутов, по соглашению при вы­ зове метода MapRoute () указываются имена аргументов, что объясняет наличие в листинге явно именованных аргументов name и template. В аргументе name указы­ вается имя маршрута, а в аргументе template определяется шаблон. Совет. Назначать маршрутам имена не обязательно, и существует философский аргумент относительно того, что именование приносит в жертву четкое разделение обязанностей, которое в противном случае обеспечила бы маршрутизация. В разделе "Генерирование URL из специфического маршрута" главы стать проблемой. 16 объясняется, почему именование может Глава 15. Маршрутизация URL 443 Запустив пример приложения. можно увидеть эффект от изменений. внесенных в маршрутизацию. При первом запуске приложения ничего не изменится жнему возникает ошибка лону 404, но если перейти на {controller} / {action}. URL. - по-пре­ который соответствует шаб­ тогда получится результат, подобный приведенному на рис. 15.З. где иллюстрируется переход на /Admin /Index. <D localhost 54839/Adnм/lndex С Controller: AdminController Action: lndex Рис. 15.З. Навигация с использованием простого маршрута Корневой в файле URL для приложения не работает из-за того. что маршрут. добавленный Startup.cs. не сообщает инфраструктуре контроллера и метод действия. когда URL запроса MVC о том. как выбирать класс не содержит сегментов. Мы испра­ вим это в следующем разделе. Определение стандартных значений Пример приложения возвращает ошибку ный URL. 404. когда запрашивается стандарт­ поскольку он не соответствует шаблону маршрута. определ енному в классе StaL· t up . Так как в стандартном ветствовать переменным URL отсутствуют contr o lle r и act i o n. сегменты. которые могли бы соот­ определенным в шаблоне маршрута. система маршрутизации не дает совпадения. Ранее объяснялось. что шаблоны URL будут соответствовать только URL с указан­ ным количеством сегментов. Один из способов изменить такое поведение предусмат­ ривает применение стандартных значений. Стандартное значение используется . когда URL не содержит сегмент. который можно было бы сопоставить с шаблоном маршрута . В листинге 15. 10 определен маршрут. в котором применяется стандарт­ ное значени е. Листинг 15.1 О. Предоставление стандартного значения в файле из папки us ing using us i ng u s i ng using using u s ing using UrlsAndRoutes System ; System.Co lle c tions.Gen er i c; Sy s t e m.Linq; Sy stem.Th reading.Tas ks; Microsoft.AspNetCore.Bu i l de r; Microsoft.Asp NetCore.Hos ting ; Microsoft.AspNet Core.Http; Micr osof t.Extensi o ns.Dependenc yinjec ti on; namespace Url s AndRout es puЫi c c l a ss S t a r tup { Startup. cs 444 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 void ConfigureServices (IServiceColle ctio n services) services.AddMvc(); puЬli c void Configure (IAppli cationBuilder app.UseDevel operExcepti o n Page (); app.UseStatu s CodePages( ) ; app.UseStaticFiles(); puЫic app.UseМvc(routes арр, IHostingEnvi r onment env ) { => { routes.МapRoute( name: "default", template: "{controller}/{action}", defaul ts: new { action = "Index" } ) ; }) ; Стандартные значения задаются как свойства анонимного типа, передаваемого методу MapRoute () в аргументе de fa ul ts. В листинге 15.10 для I n dex. переменной acti on предоставлено стандартное значение URL. URL вида ht tp : / /mydoma i n . com/Home / Index маршрут извлечет Home в качестве значения для control1er и I ndex - для act ion. Но теперь. когда имеется стандартное значение для сегмента ас t i on. маршрут будет также соответствовать и односегментным URL. При обработке односегментно­ го URL система маршрутизации извлечет значение для переменной c on tr o1ler из URL и будет применять стандартное значение для переменной acti on . Таким обра­ зом, запрос пользователем / Home приведет к тому. что MVC вызовет метод действия Index () контроллера Home (рис. 15.4). Как и ранее, этот маршрут будет соответствовать всем двухсегментным Например. в случае запроса С Ф localhost S4 Controller: AdminController Action: lndex Рис. 15.4. Использование стандартного действия Определение встраиваемых стандартных значений Станда ртные значения могут также быть выражены как часть шаблона URL, что дает более лаконичный способ определения маршрутов. как показано в листин­ ге 15. 11. Встраиваемый синтаксис может применяться только для предоставления стандартных значений переменным. которые являются частью шаблона URL. но. как вскоре вы узнаете. зачастую удобно иметь возможность устанавливать стандартные значения за пределами шаблона. По указанной причине важно понимать оба способа выражения стандартных значе ний . Глава 15. Маршрутизация URL Листинг 15.11. 445 Определение встраиваемых стандартных значений в файле Startup.cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; using using using using using using using using namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", puЫic арр, IHostingEnvironment env) { template: "{controller}/{action=Index}"); 1); Мы можем пойти еще дальше и сопоставлять с URL, которые вообще не содержат переменных сегментов, полагаясь при идентификации действия и контроллера толь­ ко на стандартные значения. Для примера в листинге ражение корневого URL 15.12 демонстрируется отоб­ приложения за счет предоставления стандартных значений для обоих сегментов. Листинг 15.12. Предоставление стандартных значений для переменных и using using using using using using using using action в файле Startup.cs из папки controller UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЬlic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic 446 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 void Configure( I ApplicationBuilder app.Us eDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app .UseMvc(ro utes => { routes.MapRoute( name: "defaul t", puЫic арр, IHos tingEnvi ronment env) { template: "{controller=Home}/{action=Index}"}; }) ; За счет предоставления стандартных значений для переменных action маршрут (табл. 15.4). Таблица будет соответствовать URL co ntroller 15.4. Соответствие URL Количество сегментов На что отображается Пример 1 controller=Home, action =Index /Customer controller=Customer, action=Index 2 /Customer/List controller=Customer, action=List 3 /C ustomer/List/All Соответствия нет о и с нулем. одним или двумя сегментами Чем меньше сегментов получено во входящем URL. - сегментов слишком много тем больше маршрут полагает­ ся на стандартные значения, вплоть до момента, когда URL без сегментов сопостав­ ляется с использованием только стандартных значений. Запустив пример приложения, можно увидеть результат применения стандар­ тных значений. Когда браузер запрашивает корневой менных сегментов controller и acti on чения, которые приведут к тому, что инфраструктура Index () контроллера Home С\ Routing С (рис. MVC вызовет метод действия х ф locafhost 54339 HomeController Action: lndex 15. 5. приложения. для пере­ 15.5). Controller: Рис. URL будут использоваться стандартные зна ­ Применение стандартных значений для расширения области действия маршрута Глава 15. Маршрутизация URL Использование статических сегментов Не все сегменты в шаблоне URL 447 URL должны быть переменными. Допускается так­ же создавать шаблоны, имеющие статические сегменты. Предположим, что при­ ложение нуждается в сопоставлении с URL, которые снабжены префиксом PuЬlic, например: http://mydomain.com/PuЫic/Home/Index Это можно сделать с применением шаблона тинге Листинг 15.13. подобного показанному в лис­ Использование статических сегментов в файле из папки using using using using using using using using URL, 15.13. Startup. cs UrlsAnd.Routes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}"); puЫic routes.мapRoute(name: template: "", "PuЬlic/{controller=Home}/{action=Index}"); }) ; Новый шаблон будет соответствовать только URL, содержащим три сегмента, пер­ вым из которых должен быть PuЫic. Остальные два сегмента могут содержать лю­ бые значения, и они будут использоваться для переменных controller и action. Если последние два сегмента не указаны, тогда будут применяться стандартные значения. Можно также создавать шаблоны URL, которые имеют сегменты, содержащие как статические, так и переменные элементы, вроде шаблона в листинге 15.14. 448 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Листинг using using using using using using using using 15.14. Смешивание сегментов в файле Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplication8uilder арр, IHostingEnvironment env ) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes .МapRoute ("", "Х{ controller} / {action} ") ; routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}"); routes.MapRoute(name: "", template: "PuЬlic/{controller = Home}/{action=Index}"I; }) ; Шаблон в показанном маршруте соответствует любому двухсегментному котором первый сегмент начинается с буквы Х . Значение для первого сегмента, исключая Х. Значение для action CJ R.outing С URL вида /XHome/ Index. (рис. 15.6). ф localhost 54839/XHor.e/lndex HomeController Action: lndex в можно увидеть эффект х Controller: URL. берется из получается из второго сегмента. Запустив приложение и перейдя на от добавления нового маршрута controller Рис. 15.б. Смешивание в одном сегменте статических и переменных элементов Глава 15. Маршрутизация URL 449 Упорядочение маршрутов В листинге 15.14 новый маршрут был определен и помещен перед другими маршрутами. Я поступил так потому, что маршруты применяются в порядке, в котором они определе­ ны. Метод MapRoute () добавляет маршрут в конец конфигурации маршрутизации, а это означает, что обычно маршруты применяются в порядке их определения. Формулировка "обычно" объясняется тем, что есть методы, которые позволяют вставлять маршруты в спе­ цифические позиции. Я стараюсь не использовать такие методы, поскольку применение маршрутов в порядке их определения упрощает понимание маршрутизации в приложении. Система маршрутизации пытается сопоставить входящий URL с шаблоном URL маршрута, который был определен первым, и продолжает сопоставление со следующим маршрутом только в случае, если не произошло совпадение. Маршруты проверяются последовательно, пока не обнаружится соответствие или не исчерпается набор маршрутов. По указанной при­ чине самые специфичные маршруты должны быть определены первыми. Маршрут, добав­ ленный в листинге 15.14, является более специфичным, чем следующие за ним маршруты. Предположим, что порядок следования маршрутов изменен на обратный: routes.MapRoute("MyRoute", "{controller=Horne)/{action=Index)"); r о u t е s . Мар Rо u t е ( " " , "Х { с о n t r о 11 е r ) / {а с t i о n ) " ) ; Тогда первый маршрут, который соответствует любому URL с нулем, одним или двумя сегмен­ тами, будет использоваться всегда. Более специфичный маршрут, который теперь второй в списке, никогда не будет достигнут. Новый маршрут исключает ведущую букву Х из это не будет сделано из-за совпадения со старым маршрутом. Таким образом, URL URL, но вида: http://rnydornain.com/XHorne/Index будет нацелен на контроллер по имени класса XHomeController Статические сегменты XHorne, предполагая существование в приложении и наличие в нем метода действия URL Index (). и стандартные значения можно комбинировать с це­ URL. После развертывания приложе­ URL формирует контракт с пользователями, и если в будущем производится рефакторинг приложения, то предыдущий формат URL необходимо предохранить, чтобы любые URL, добавленные в закладки, макросы или сценарии, лью создания псевдонима для специфического ния применяемая схема написанные пользователем, продолжили работать. Представим, что у нас использовался контроллер по имени заменен контроллером Home. предохранения старой схемы Листинг 15.15 Shop, который теперь показано, как создать маршрут для URL. 15.15. Сегменты и стандартные значения в файле Startup. cs из папки using using using using using using using using В листинге UrlsAndRoutes Systern; Systern.Collections.Generic; Systern.Linq; Systern.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; 450 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 n amespace Url s AndR o u tes ( puЫic clas s Startup ( puЫic v oi d Configur eSe rvices( ISe r v iceColl ec tion servi ces ) ( servi c e s .AddMvc () ; puЫ ic void Con f igu r e(I Applica tionB u ilder app. Us e De veloperE xc e p tionPag e( ); app.Us e S t atusCod ePag es(); app.Us e StaticFil es (); app. Us eM vc( r o ut es => { а рр , I Hos ting Environment e nv ) ( routes.мapRoute( name: "ShopSchema" , template: "Shop/ {action}", defaults: new { controller = rout e s. Ma pRo ute ( "", "Х "Ноше" }) ; { contr o l l er) / { a ct i o n) " ) ; r o u tes .MapRoute( name : "default", t e mpl a te: "{c o n tro ller=H o me) /(action =Ind e x)"); rout es .MapRoute(n a me: "", templa te: "PuЬli c/ (contr o l l e r= Home) / { actio n=Ind e x ) " ) ; }) ; Добавленный маршрут соответствует любому двухсегментному п е рвым сегментом является Шаблон URL Shop . Значение a c ti on не содержит переменного сегмента для ся стандартное значение. Аргумент defa ults URL, в котором берется из второго сегмента cont r o ller. URL. поэтому выбирает­ предоставляет значение controll er . потому что отсутствует какой-либо сегмент, к которому можно было бы применить значение как часть шаблона URL. В результате запрос действия из контроллера Shop транслируется в запрос для URL вида / Shop/ Index. можно оценить эффект от добавления такого маршрута. Как видно на рис. 15.7, новый мар­ шрут приводит к тому. что инфраструктура MVC вызывает метод действия Index () контроллера Home . Запустив приложение и перейдя на из контроллера Н оте. С 1 <D loca l host :54839/Shop/lлdex Controller: HomeController Action: lпdex Рис. 15. 7. Создание псевдонима для предохранения схемы URL Глава 15. Маршрутизация URL 451 Можно продвинуться на шаг дальше и создать псевдонимы также для методов действий, которые в результате рефакторинга перестали суmествовать в контролле­ URL 15.16). ре. Чтобы сделать это. понадобится просто создать статический стандартные значения для Листинг 15.16. и action (листинг и предоставить Создание псевдонима для контроллера и действия в файле из папки using using using using using using using using controller Startup. cs UrlsAndRoutes System; System.Collections.Gener ic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Bui lder; Microsoft.AspNetCore.Hos ting; Microsoft.AspNetCore.Http ; Microsoft.Extensions.Dep endencyinjection; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServi ceCollection services) { services.AddMvc(); puЫic void Configure ( IApplicationBuilder app.UseDeveloperExceptio nPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { puЫic арр, IHostingEnvironment env) { routes.мapRoute( name: "ShopSchema2" , template: "Shop/OldAction", defaults: new { controller "Home", action "Index" } ) ; routes.MapRoute( name: "ShopS chema", template: "Shop/ { action} ", defaults: new { controller = "Home" }); routes. MapRoute ( "", "Х { control ler} / { action} ") ; routes.MapRoute( name: "default", template: "{controller=Home}/{actio n=Index}"); routes.MapRoute(name: "", template: "PuЬlic/{controller=Home}/{action=Index}"); }) ; Обратите внимание. что новый маршрут определен первым, поскольку он явля­ ется более специфичным. чем маршруты, следующие после него. Если запрос для Shop/OldAction обработается, скажем, следующим определенным маршрутом, тогда может быть получен результат, отличающийся от того, который желательно получить при наличии контроллера с методом действия OldAction (). Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC 2 452 Определение специальных переменных сегментов Переменные сегментов в приложениях MVC controller и action имеют специальное предназначение и соответствуют контроллеру и методу действия, которые будут использоваться для обслуживания запроса. Они являются всего лишь встроенными переменными сегментов. и допускается также определять специальные переменные сегментов. как показано в листинге 15. 17. (Маршруты. определенные в предыдущем разделе, удалены, чтобы можно было начать сначала.) Листинг 15.17. Определение дополнительных переменных внутри шаблона в файле using using using using using using using using Startup. cs из папки URL UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Bu1lder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes ( puЫic class Startup ( puЫic void ConfigureServices(IServiceCollection services) ( services.AddMvc(); puЫic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => ( арр, IHostingEnvironment env) { routes .МapRoute (name: "МyRoute", template: "(controller=Home}/{action=Index}/{id=Defaultid}"); ) ) ; Шаблон URL определяет стандартные переменные специальную переменную по имени id. controller и action плюс URL с ко­ Маршрут будет соответствовать личеством сегментов от нуля до трех. Содержимое третьего сегмента присваивает­ ся переменной id, а при отсутствии третьего сегмента применяется стандартное значение. Внимание! Некоторые имена зарезервированы и не доступны для использования в качестве имен специальных переменных сегментов. К ним относятся Назначение первых двух очевидно, а области В классе (area) controller, action и area. обсуждаются в следующей главе. Controller, который является базовым для контроллеров, определено RouteData. Оно возвращает объект Microsoft .AspNetCore. Routing. RouteData, предоставляющий детали о системе маршрутизации и о способе, ко- свойство Глава 15. Маршрутизация URL 453 торым маршрутизировался текущий запрос. Внутри контроллера можно получать доступ к любой переменной сегмента в методе действия с применением свойства Ro u teData. Values, которое возвращает словарь, содержащий переменные сегмен­ та. В целях демонстрации в контроллер Ноте добавлен метод действия по имени CustomVariaЫe () ,приведенный в листинге Листинг 15.18. 15.18. Доступ к специальной переменной сегмента внутри метода действия в файле HomeController. cs из папки Controllers us i ng Microsoft.AspNetCore.Mvc ; using Ur lsAndRout es.Models; namespace UrlsAndRoutes.Con t r o ll e rs puЬlic class HomeContro ll er : Control l er ViewResu lt Index( ) => View("Result", new Resul t { Controller = nameof ( HomeControl l er), Action = nameof ( Index) puЬlic }) ; ViewResul t CustomVariaЬle () Result r = new Result ( Controller = nameof(HomeController), Action = nameof(CustomVariaЬle), puЫic } ; r.Data["Id"] = RouteData.Values["id"]; return View("Result", r); Метод действия Cust omV a riaЫ e id в шаблоне URL () получает значение специальной переменной маршрута, используя свойство RouteData. Values, которое воз­ вращает словарь переменных. порожденных системой маршрутизации. Специальная переменная добавляется к объекту модели представления и может быть просмотрена путем запуска приложения и запроса следующего URL: / Home/CustomVariaЫe/Hello Шаблон м аршрута распознает третий сегмент в данном ременной i d, давая приведенный на рис. Рис. 15.8 URL как значение для пе­ результат. Controller: HomeController Action: CustomVariaЫe ld: Hello 15.8. Отображение значения специальной п еременной сегмента 454 Часть 11. Подробные сведения об инфраструктуре ASP NET Саге MVC 2 Шаблон URL в листинге 15.17 определяет стандартное значение для сегмента т.е. маршрут может также соответствовать URL. id . которые имеют два сегмента. Увидеть применение стандартного значения можно. запросив такой URL: /Home/CustomVariaЫe Система маршрутизации использует стандартное значение для специальной пере­ менной, как показано на рис. f- Рис. С 15.9. Q) loca lhost .54v39/Ho1YJe/CustoniVar1aЫe Controller: HomeController Action: CustomVariaЫe ld: Defaultld 15.9. Стандартное значение для специальной переменной сегмента Использование специальных переменных в качестве параметров метода действия Работа с коллекцией RouteData. Values - лишь один способ доступа к специ­ альным переменным маршрута: другой способ может оказаться намного более эле­ гантным. Если метод действия определяет параметры с именами. которые совпадают с именами переменных шаблона URL, то инфраструктура MVC будет автоматически URL. в виде аргументов. Специальная переменная. определенная в маршруте из листинга 15.17, имела имя id. передавать методу действия значения. извлеченные из Модифицируем метод действия CustomVariaЫe () принимал параметр с тем же самым именем (листинг Листинг 15.19. Controllers using Microsoft.AspNetC ore .Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class HomeController : Controller ViewResult Index() => View("Result", new Result { Controller = nameof(HomeController), Action = nameof(Index) puЫic )) ; puhlic ViewResult CustomVariaЬle(string Result r = new Result ( Contr o ller = name of( HomeContr o ller), Action = nameof( Custo mVariaЬle ) , id) Ho me. чтобы он 15. 19). Добавление параметра к действию в файле из папки }; в контроллере HomeController. cs Глава 15. Маршрутизация URL 455 r.Data["Id"] = id; return View( "Result", r ) ; Когда система маршрутизации обнаруживает соответствие какого - то 15.17. сваивается специальной переменной Инфраструктура id . URL марш ­ URL при ­ значение третьего сегмента руту. который был определен в листинге MVC сравнивает список пе­ ременных сегментов со списком параметров метода действия. и в случае совпадения имен передает значения из Типом параметра чение из URL данному является id методу. s t r in g. URL в любой тип, указанный для твия объявлен как int или DateTime. но MVC будет пытаться преобразовать зна­ параметра. Если параметр id тогда полученное значение из метода дейс­ URL окажется преобразованным в экземпляр этого типа . Такое элегантное и удобное средство из­ бавляет от необходимости обрабатывать преобразования самостоятельно. Запустив приложение и запросив URL вида /Hom e / CustomVa r iaЬl e / Hel lo , можно увидеть эффект от параметра метода действия (рис. 15.1 О). Если опустить третий сегмент, тогда методу действия будет предоставлено стандартное значение для переменной сегмента. которое также видно на рис . D Rout8'g 15.10. х с Рис. С Controller: HomeController Action: CustomVariaЫe ld : Hello 15.1 О. ф loиlhostS·i Controller: HomeController Action: CustomVariaЫe ld: Defauhld Доступ к переменным сегментов с применением параметров метода действия URL, в типы .NET инф­ MVC использует средство привязки моделей, которое может обрабатывать На заметку! Для преобразования значений, содержащихся внутри раструктура намного более сложные ситуации, чем продемонстрированная в приведенном выше при ­ мере . Привязка моделей будет описана в главе 26. Определение необязательных сегментов Необязательным является такой сегмент URL URL, который пользователь может не указывать и для которого не предусмотрено стандартного значения. Н ео бя зател ьный сегмент обозначается знаком вопроса (символом ?) после им ени се гм ента. как пока­ зано в ли стинге Листинг 15.20. 15.20. Определение необязательного сегмента из папки UrlsAndRoutes us i ng System; u sing System . Co llections . Generi c ; URL в файле Startup. cs Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 456 using using using using using using System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc{); puЫic void Configure{IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute(name: "MyRoute", template: "{controller=Home}/{action=Index}/{id?}"); } ) ; Данный маршрут будет соответствовать id. В табл. Таблица 15.5 15.5. Количество сегментов URL, имеющим или не имеющим сегмента иллюстрируется его работа с различными Соответствие URL с URL. необязательной переменной сегмента ПримерURL На что отображается / controller=Horne,action=Index /Customer controller=Customer,action=Index 2 /Customer/List controller=Custorner,action=List 3 /Customer/List/All controller=Custorner,action=List id=All 4 /Customer/List/All/Delete Соответствия нет о - сегментов слишком много В табл. 15.5 видно. что переменная когда во входящем URL id добавляется к набору переменных, только присутствует соответствующий сегмент. Такая возможность удобна, когда необходимо узнать, предоставил ли пользователь значение для пере­ менной сегмента. Если для необязательной переменной сегмента значение не было задано, тогда значением соответствующего параметра будет контроллер id Home null. В листинге значение не указано. Листинг 15.21. 15.21 обновлен с целью учета ситуации. когда для переменной сегмента Проверка необязательной переменной сегмента в файле HomeController. cs using Microsoft.AspNetCore.Mvc; из папки Controllers 457 Глава 15. Маршрутизация URL using UrlsAndRo utes.Models; namespace UrlsAndRoutes.Co ntroll ers puЬlic class HomeController : Controller ViewResu l t Index () => View ( "Resul t", new Result { Contr o ller = nameo f (Home Co ntr o ller ) , Act i o n = name o f(Ind e x) puЫic }) ; ViewRe s ult CustomV ar ia Ьle(string i d ) { Result r = new Result { Co ntroller = nameof(H omeCo ntroller ) , Ac ti o n = name o f(Cust om Va r i aЬle), puЫic }; r.Data["Id"] =id?? "<novalue>"; re t ur n View ( "Result", r ) ; На рис. 15. 11 приведен результат запуска приложения и перехода по URL вида / H ome /Cu s t omVari aЫ e , в котором не включено значение для переменной сегмента С <D localhos .548.:19/Hon1ejCustomVar1aole Controtler: HomeContf'Oller Action: CustomVariaЫe ld : <no value> Рис. 15.11. id. Обнаружение ситуации, когда URL не содержит значения для необязательной переменной сегмента Стандартная конфигурация маршрутизации MVC добавляется в UseMvc Wi thDef aul tRo ute (), который Инфраструктура классе St а r t u p с применением метода является просто удобным методом для настрой­ ки наиболее распространенной конфигурации маршрутизации и эквивалентен следующему коду: app.UseMvc( ro u tes => r o utes.MapRoute( name: "def a u lt ", t e mplate: " {con t r o ll er=Home }/ {a ct i o n=Inde x ) / {id? f"); }) ; 458 Часть 11. Подробные сведения об инфраструктуре ASP. NET Саге MVC 2 Такая стандартная конфигурация обеспечивает сопоставление с URL, которые нацелены на классы контроллеров и методы действий по их именам, а также могут содержать необязатель­ ный сегмент id. Если сегмент controller или action опущен, тогда используются стан­ дартные значения, чтобы направить запрос контроллеру Ноте и методу действия Index (). Определение маршрутов переменной длины Еще один способ изменения консервативного стандартного поведения шаблонов URL предусматривает прием переменного количества сегментов маршрутизировать URL URL. Это позволяет произвольной длины в единственном маршруте. Поддержка переменного числа сегментов определяется путем назначения одной из переменных сегментов в качестве переменной общего захвата, для чего она предваряется симво­ лом звездочки (листинг Листинг 15.22. 15.22). Назначение переменной общего захвата в файле из папки Startup. cs UrlsAndRoutes using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Dependencylnjection; 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: "MyRou te", template: "{controller=Home}/{action=Index}/{id?}/{*catchall}"); }) ; Здесь был расширен маршрут из предыдущего примера за счет добавления пере­ менной общего захвата по имени твовать любому URL catchall. Теперь такой маршрут будет соответс­ вне зависимости от количества содержащихся в нем сегментов либо их значений. Первые три сегмента применяются для установки значений пере­ менных controller, action и id. Если все они присваиваются переменной В листинге твие List модели. 15.23 URL содержит catchall, дополнительные сегменты. то как показано в табл. приведен обновленный контроллер передает представлению значение 15.6. Customer, в котором дейс­ переменной catchall через объект Глава 15. Маршрутизация URL Таблица 459 15.6. Сопоставление URL с переменной общего захвата Количество сегментов Пример На что отображается URL / controller=Home,action=Index /Customer controller=Customer, action=Index 2 /Customer/List controller=Customer, action=List 3 /Customer/List/All controller=Customer, action=List,id=All 4 /Customer/List/All/Delete controller=Customer, action=List, id=All, catchall=Delete 5 /Customer/List/All/Delete/Perm controller=Customer, action=List, id=All, catchall=Delete/Perm о Листинг 15.23. Модификация метода действия в файле из папки CustomerController.cs Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class CustomerController : Controller ViewResult Index() => View("Result", new Resul t { Controller = nameof(CustomerController), Action = nameof(Index) puЬlic }) ; ViewResult List(string id) Resul t r = new Resul t ( Controller = nameof(HomeController), Action = nameof(List), puЫic }; r.Data["Id"] = id ?? "<no value>"; r.Data["catchall"] = RouteData.Values["catchall"]; return View("Result", r); Чтобы протестировать сегмент общего захвата, понадобится запустить приложе­ ние и запросить следующий URL: /Customer/List/Hello/1/2/3 Верхнего предела количества сегментов, для которого шаблон руте будет давать совпадение, не существует. На рис. 15.12 URL в этом марш­ показан эффект от опре- 460 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 деления сегмента общего захвата. Обратите внимание. что сегменты. попадающие в переменную общего захвата. представлены в форме сегмент/ сегмент/ сегмент. и мы сами отвечаем за обработку строки для ее разбиения на отдельные сегменты. ~ С ф localhost·S4839/Custoiner/l1st/Hello/1/2/3 Controller: HomeController Action: List ld: Hello catchall: 1/2/3 Рис. 15.12. * Использование сегмента общего захвата Ограничение маршрутов В начале главы упоминалось, что шаблоны URL консервативны в отношении коли­ чества сопоставляемых сегментов и либеральны в отношении содержимого сопостав­ ляемых сегментов. В нескольких предшествующих разделах объяснялись различные приемы управления степенью консервативности: увеличение или уменьшение числа сегментов с применением стандартных значений. необязательных переменных и т.д. Теперь наступило время взглянуть на то, каким образом управлять либеральнос­ тью при сопоставлении с содержимым сегментов набор URL, URL. а именно - с которыми будет сопоставляться маршрут. В листинге руется использование простого ограничения URL. как ограничить 15.24 демонстри­ с которыми будет сопоставляться маршрут. Листинг using using using using using using using using 15.24. Ограничение маршрута в файле Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes { puЫic class Startup { puЬlic void ConfigureServices(IServiceCollection services) ( services.AddMvc(); 461 Глава 15. Маршрутизация URL void Configure(IApplicationBuil der арр, IHostingEnvironment env) { app.UseDeveloperExceptio nPage(); app.UseStatusCodePages(); app.UsestaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRou te", template: "{controller=Home}/{actio n=Index}/{id:int?}"); puЫic )); Ограничения отделяются от имени переменной сегмента двоеточием (символ Ограничением в листинге является int. и оно применяется к сегменту мер встраиваемого ограничения, которое определено как часть шаблона : ). Это при­ id. URL и при­ менено к одиночному сегменту: template: "{controller)/{action)/{ id:int?)", Ограничение in t разрешает сопоставление шаблона значение которых может быть преобразовано в тип так что маршрут будет сопоставляться с URL без сегмента ет, то должен быть целочисленным значением (в табл. Таблица Пример 15. 7. Сопоставление URL URL только с сегментами, Сегмент int. id, 15.7 необязателен, id но если он присутству­ приведены примеры). с ограничением На что отображается URL 1 controller=Home,action=In dex, id=null /Home/CustomVariaЬle/Hello Соответствия нет - разован в значение сегмент id не может быть преоб­ in t /Home/CustomVariaЬle/l controller=Home,action=CustomVariaЫe, /Home/CustomVariaЬle/l/2 Соответствия нет - сегментов слишком много Ограничения также могут быть указаны за пределами шаблона зованием аргумента constraints метода id=l MapRoute () URL с исполь­ при определении маршрута. Такой прием удобен, если предпочтение отдается хранению шаблона URL отдельно от ограничений или соблюдению стиля маршрутизации, принятого в более ранних версиях MVC, где встраиваемые ограничения не поддерживались. В листинге 15.25 показано то же самое ограничение in t на переменной сегмента id, выраженное с применением отдельного ограничения. При использовании такого формата стандар­ тные значения также выражаются внешне. Листинг 15.25. Выражение ограничения за пределами шаблона в файле Startup. cs из папки using System; using System.Collections.Gener ic; using System.Linq; UrlsAndRoutes URL 462 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Dependencylnjection; using Microsoft.AspNetCore.Routing.Constraints; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddMvc(); void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controller}/{action}/{id?} 11 , defaul ts : new { con troller = "Home 11 , action = 11 Index 11 } , constraints: new { id = new IntRouteConstraint() }) ; puЫic }) ; Аргумент constraints метода MapRoute () определен с применением анонимного типа, имена свойств которого соответствуют ограничиваемым переменным сегмен­ тов. Пространство имен Microsoft .AspNetCore. Routing. Constraints содержит набор классов, которые можно использовать для определения индивидуальных огра­ ничений. В листинге 15.25 аргумент constraints настроен на применение объекта IntRouteConstraint для сегмента id, что дает такой же результат, как встраивае­ мое ограничение из листинга 15.24. В табл. 15.8 описан полный набор классов ограничений в пространстве имен Microsoft. AspNetCore. Routing. Constraints вместе с их встраиваемыми экви­ валентами для ограничений, которые могут применяться к одиночным элементам в шаблоне Табпица URL; 15.8. Встраиваемое ограничение alpha часть из них будет рассматриваться в последующих разделах. Ограничения маршрутов на уровне сегментов Описание Конструктор кпасса Соответствует алфавитным AlphaRouteConstraint() символам независимо от ре- гистра bool (A-Z, a-z) Соответствует значению, BoolRouteConstraint () которое может быть преобразовано в тип datetime bool Соответствует значению, ко- торое может быть преобразовано в тип DateTime DateTimeRouteConstraint () Глава 15. Маршрутизация URL Окончание табл. Встраиваемое ограничение decimal Описание Конструктор класса Соответствует значению, DecimalRouteConstraint() 463 15.8 которое может быть преоб­ разовано в тип douЫe decimal Соответствует значению, DouЬleRouteConstraint() которое может быть преоб­ разовано в тип douЫe float Соответствует значению, FloatRouteConstraint() которое может быть преоб­ разовано в тип guid float Соответствует значению GuidRouteConstraint() глобально уникального идентификатора (globally unique identifier - GUID) int Соответствует значению, IntRouteConstraint() которое может быть преоб­ разовано в тип length(len) length(min,max) int Соответствует строковому LengthRouteConstraint(len) значению, которое содер­ жит указанное количество LengthRouteConstraint(min, max) СИМВОЛОВ ИЛИ ЧИСЛО СИМ­ ВОЛОВ между min и max (включительно) long Соответствует значению, LongRouteConstraint() которое может быть преоб­ разовано в тип maxlength(len) long Соответствует строковому MaxLengthRouteConstraint(len) значению с количеством символов не более max (val) Соответствует значению int, minlength(len) len если оно меньше MaxRouteConstraint(val) val Соответствует строковому MinLengthRouteConstraint(len) значению, которое име- ет, по крайней мере, len символов min (val) Соответствует значению int, range(min,max) если оно больше Соответствует значению int, MinRouteConstraint(val) val RangeRouteConstraint(min,max) если оно нахо­ дится между min и max (включительно) regex(expr) Соответствует регулярному выражению RegexRouteConstraint(expr) 464 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Совет. Можно ограничить доступ к методам действий со стороны запросов, которые сдела­ ны с помощью специфических НТТР-команд, таких как GET или POST, используя набор 17 описано применение этих атрибутов для обработки форм в контроллерах, а в главе 20 приведен полный список до­ атрибутов MVC наподобие HttpGet и HttpPost. В главе ступных атрибутов. Ограничение маршрута с использованием регулярного выражения Наибольшую гибкость предлагает ограничение regex, мент с применением регулярного выражения. В листинге ограничивается диапазоном Листинг 15.26. URL, сегмент controller с которыми он будет сопоставляться. Использование регулярного выражения для ограничения маршрута в файле using using using using using using using using using которое сопоставляет сег­ 15.26 Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; namespace UrlsAndRoutes { puЫic class Startup { puЬlic void ConfigureServices(IServiceCollection services) { services.AddMvc(); void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", teш.plate: "{controller:regex("H.*)=Home}/{action=Index}/{id ?}"); puЫic )) ; Примененное к маршруту ограничение обеспечивает его соответствие только та­ ким URL. в которых сегмент controller начинается с буквы Н. На заметку! Стандартные значения используются перед проверкой ограничений. Таким об­ разом, например, при запросе URL вида /для переменной сегмента controller при­ Horne. Затем проверяются ограничения, и поскольку зна­ controller начинается с Н, стандартный URL будет соответствовать маршруту. меняется стандартное значение чение Глава 15. Маршрутизация URL 465 С помощью регулярных выражений можно ограничивать маршрут так, что к совпадению будут приводить только специфические значения для какого-то сегмен­ та URL. Это делается с использованием символа URL (Шаблон 1. как показано в листинге 15.27. разбит на две части, чтобы уместиться на печатной странице, о чем заботиться в реальном проекте нет нужды.) Листинг 15.27. Ограничение маршрута специфическим набором значений переменной сегмента в файле using using using using using using using using using Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; namespace UrlsAndRoutes { puЬlic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controller:regex("B.*)=Bome)/" + "{action:regex("Index$1"AЬout$)=Index}/{id?)"); puЫic }) ; Такое ограничение разрешает маршруту соответствовать только имеют сегмент action со значением Index совместно, так что ограничения, наложенные на значение переменной бинируются с ограничениями, наложенными на значение переменной Другими словами, маршрут в листинге URL, ем переменной action является 15.27 которые action, ком­ controller. будет соответствовать только таким controller начинается Index или AЬout. в которых значение переменной URL, или AЬout. Ограничения применяются с буквы Н, а значени­ Использование ограничений на основе типов и значений Большинство ограничений применяются для ограничения маршрутов, чтобы они соответствовали только URL с сегментами, значения которых могут быть преобразо­ ваны в указанный тип либо имеют специфический формат. Хорошим примером может служить ограничение int, использованное в начале этого раздела: маршруты будут 466 Часть 11. Подробные сведения об инфраструктуре ASP. NET Core MVC 2 соответствовать, только когда значение ограниченного сегмента может быть преоб­ разовано в значение .NЕТ-типа ограничения range, int. В листинге 15.28 демонстрируется применение которое ограничивает маршрут так, что он соответствует только если значение сегмента может быть преобразовано в in t URL. и находится между указанными значениями. Листинг 15.28. Ограничение на основе типа и значения в файпе из папки using using using using using using using using using Startup. cs UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; Microsoft.AspNetCore.Routing.Constraints; namespace UrlsAndRoutes { puЬlic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controller=Hoш.e}/{action=Index}/ {id:range(l0,20)?}"); puЫic ) ) ; Ограничение в рассмотренном примере применялось к необязательному сегменту id. Ограничение будет проигнорировано, если мере. три сегмента. Если сегмент URL, 20 URL запроса не содержит, по крайней присутствует, то маршрут будет соответствовать только когда значение сегмента может быть преобразовано в диапазон между и id 10 и 20. Ограничение range является in t и попадает в включающим, т.е. значения 10 считаются входящими в диапазон. Объединение ограничений Если необходимо применить к одиночному элементу сразу несколько ограничений, тогда их можно соединить в цепочку. отделяя каждое ограничение от других двоето­ чием (листинг 15.29). Глава 15. Маршрутизация URL Листинг 467 15.29. Объединение встраиваемых ограничений в файле Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; Microsoft.AspNetCore.Routing.Constraints; using using using using using using using using using namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironrnent env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controller=Hoшe}/{action=Index}" + "/{id:alpha:шinlength(б)?}"); puЫic )) ; В листинге 15.29 к сегменту id были применены ограничения alpha и minlength. Знак вопроса, обозначающий необязательный сегмент, учитывается после всех ог­ раничений. В результате объединения ограничений маршрут будет соответствовать URL, только когда сегмент id опущен (поскольку он необязателен) или когда он при­ сутствует и содержит, по крайней мере, шесть алфавитных символов. Если вы не используете встраиваемые ограничения, то должны применять класс Microsoft. AspNetCore. Routing. Composi teRouteConstraint, который позволя­ ет ассоциировать несколько ограничений с одиночным свойством в анонимно типи­ зированном объекте. В листинге использовались в листинге Листинг 15.30. показана комбинация ограничений, которые Объединение отдельных ограничений в файле из папки using using using using using using 15.30 15.29. UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Startup. cs Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 468 using using using using Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index" } , constraintз: new { id = new CompoзiteRouteConзtraint( new IRouteConзtrain t [] { new AlphaRouteConзtraint(), new МinLengthRouteConзtraint(б) }) }) ; }) ; Конструктор класса CompositeRouteConstraint принимает перечисление объ­ IRouteConstraint, определяющий ограни­ ектов, которые реализуют интерфейс чения маршрутов. Система маршрутизации разрешит соответствие маршрута URL. только если все ограничения удовлетворены. Определение специального ограничения Если для обеспечения текущих потребностей стандартных ограничений не хватает, тогда можно определить собственные ограничения, фейс реализовав интер­ IRouteConstraint, который находится в пространстве имен Microsoft. AspNetCore.Routing. Чтобы проверить такую возможность, добавим в пример проекта папку Infrastructure и поместим в нее новый файл класса по имени WeekDayConstraint. cs с содержимым из листинга 15.31. Листинг 15.31. Содержимое файла из папки WeekDayConstraint. сз Infrastructure using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using System.Linq; namespace UrlsAndRoutes.Infrastructure Глава 15. Маршрутизация URL 469 class WeekDayConstraint : IRouteConstraint { private static string [] Days = new [] { "mon", "tue", "wed", "thu", "fri", 11 sat", "sun" }; puЫic bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { return Days.Contains(values[routeKey]?.ToString() .ToLowerlnvariant()); puЫic В интерфейсе IRouteConstraint определен метод Match (),который вызывает­ ся, чтобы позволить ограничению решить, должен ли запрос соответствовать марш­ руту. Параметры метода Match () предоставляют доступ к запросу, поступившему от клиента, маршруту, имени ограничиваемого сегмента, переменным сегментов, извле­ ченным из URL, и признаку того, для какого или исходящего (исходящие URL URL проверяется запрос рассматриваются в главе В этом примере с помощью параметра routeKey - входящего 16). из параметра values извлекает­ ся значение переменной сегмента, к которому было применено ограничение, преобра­ зуется в строку нижнего регистра и проверяется на предмет соответствия одному из дней недели, определенных в статическом поле Days. В листинге 15.32 новое ограни­ чение применяется к маршруту с использованием синтаксиса отдельно определяемых ограничений. Листинг 15.32. Применение специального ограничения в файле из папки using using using using using using using using using using using Startup. cs UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServiceCollection services) { services.AddMvc(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", template: "{controllerl/{actionl/{id?I", puЫic Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 470 defaul ts: new { controller = "Home", action = "Index" ) , constraints: new { id = new WeekDayConstraint() }) ; ) ) ; В итоге маршрут будет соответствовать (например, /Custorner /List) URL, только если сегмент id отсутствует или совпадает с одним из дней недели, которые опре­ делены в классе ограничения (скажем, /Customer/List/Fri). Определение встраиваемого специального ограничения Настройка специального ограничения так. чтобы оно могло использовать­ ся встраиваемым образом, требует дополнительного шага по конфигурированию (листинг 15.33). Листинг 15.ЗЗ. Применение специального ограничения встраиваемым образом в файле using using using using using using using using using using using Startup.cs из папки UrlsAndRoutes System; System.Collections.Gener ic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Bui lder; Microsoft.AspNetCore.Hos ting; Microsoft.AspNetCore.Http ; Microsoft.Extensions.Dep endencyinjection; Microsoft.AspNetCore.Ro uting.Constraints; Microsoft.AspNetCore.Rou ting; UrlsAndRoutes.Infrastruc ture; namespace UrlsAndRoutes { puЫic class Startup { puЬlic void ConfigureServices(IServic eCollection services) { services.Configure<RouteO ptions>(options => options. Constraintмap. Add ( "weekday" , typeof (WeekDayConstraint) ) ) ; services.AddMvc(); puЫic void Configure(IApplicationBuil der app.UseDeveloperExceptio nPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "MyRoute", арр, IHostingEnvironment env) { template: "{controller=Home}/{actio n=Index}/{id:weekday?}"); ) ) ; Глава 15. Маршрутизация URL В методе ConfigureService () торый управляет рядом линий ConstraintMap конфигурируется объект поведения RouteOptions, системы маршрутизации. 471 ко­ Свойство возвращает словарь, используемый дЛЯ трансляции имен встраива­ емых ограничений в классы реализации IRouteConstraint, которые предоставляют логику ограничений. В словарь было добавлено новое отображение, поэтому на класс WeekDayConstraint можно ссылаться встраиваемым образом как на weekday: template: "{controller=Home)/{action=Index)/{id:weekday?)", Результат будет тем же, но настройка отображения позволяет применять специ­ альный класс ограничения встраиваемым образом. Использование маршрутизации с помощью атрибутов Во всех примерах, приведенных до сих пор в главе, маршруты определялись пос­ редством приема, который называется маршрутизацией на основе соглашений. Инфраструктура MVC также поддерживает прием. известный как маршрутизация с помощью атрибутов, когда маршруты определяются через атрибуты С#. которые применяются напрямую к классам контроллеров. В последующих разделах будет по­ казано, как создавать и конфигурировать маршруты с использованием атрибутов, которые могут свободно смешиваться с маршрутами на основе соглашений, проде­ монстрированными в предшествующих примерах. Подготовка для маршрутизации с помощью атрибутов Маршрутизация с помощью атрибутов включается при вызове метода файле Startup. cs. Инфраструктура MVC UseMvc () в исследует классы контроллеров в приложе­ нии. находит любые из них, имеющие атрибуты маршрутизации, и создает нужные маршруты. Для целей текущего раздела мы вернем стандартную конфигурацию маршрутиза­ ции в примере приложения, которая была описана во врезке "Стандартная конфигу­ рация маршрутизации" ранее в главе (листинг Листинг 15.34. Применение стандартной конфигурации в файле из папки using using using using using using using using using using using 15.34). UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes puЫic class Startup { Startup. cs 472 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 puЫic void ConfigureServices(IServiceCollection services) services.Configure<RouteOptions>(options => options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint))); services.AddMvc(); puЫic void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); арр, IHostingEnvironment env) ( app.UseМvcWithDefaultRoute(); Стандартный маршрут будет сопоставляться с URL. используя следvющий шаблон: {controllerf/{action)/{id?) Применение маршрутизации с помощью атрибутов Атрибут Route используется при указании маршрутов для индивидуальных контроллеров и действий. В листинге 15.35 атрибут Route применяется к классу CustornerController. Листинг 15.35. Применение атрибута Route в фай11е CustomerController.cs из папки Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class CustomerController : Controller [Route ("myroute")] puЫic ViewResult Index() => View("Result", new Result { Controller = nameof(CustomerController), Action = nameof ( Index) ) ) ; puЫic ViewResult List(string id) { Result r = new Result { Controller = nameof(HomeController), Action = nameof (List), ) ; r.Data["Id"] = id ?? "<no value>"; r.Data["catchall"] = RouteData.Values["catchall"]; return View("Result", r); Атрибут Rou te определяет маршрут к методу действия или контроллеру, к которому он применен. В листинге данный атрибут применен к методу действия занием rnyroute Index () сука­ в качестве маршрута, который должен использоваться. Результатом будет изменение набора маршрутов, которые применяются для достижения методов действий, определенных контроллером Custorner (табл. 15.9). Глава 15. Маршрутизация URL Таблица 15.9. Маршруть1 для контроллера Марwрут Описание /Customer/List Этот customer URL нацелен Этот URL нацелен List (),полагаясь Startup. cs на метод действия стандартный маршрут в файле /myrout.e 473 на метод действия на Index () Следует отметить два важных момента. Во-первых. когда используется атри­ бут Route. предоставленное для его конфигурирования значение применяется при myroute становится завершенным URL Index (). Во-вторых, присутствие атрибута Route определении полного маршрута, так что для достижения метода действия предотвращает использование стандартной конфигурации маршрутизации, поэто­ му метод действия Index () больше не будет достижимым с применением URL вида /Customer / Index. Изменение имени метода действия Определение уникального маршрута для одиночного метода действия в большинс­ тве приложений не особенно полезно, но атрибут гибким образом. В листинге кер [controller] 15.36 внутри Rou te можно использовать более маршрута применяется специальный мар­ для ссылки на контроллер и настройки базовой части маршрута. Совет. Изменить имя действия можно также с помощью атрибута рассматривается в главе Листинг 15.36. Переименование действия в файле из папки ActionNarne, CustomerController. cs Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers puЫic class CustomerController : Controller [Route("[controller]/МyAction")] ViewResult Index() => View("Result", new Result { Controller = nameof{CustomerController), Action = nameof(Index) puЫic 1); ViewResult List(string id) { Resul t r = new Resul t { Controller = nameof(HomeController), Action = nameof(List), 1; r.Data["Id") = id ?? "<no value>"; r.Data["catchall"J = RouteData.Values["catchall"J; return View("Result", r); puЫic который 31. 474 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Применение маркера [controller] в аргументе для атрибута Route довольно nameof и позволяет указывать маршрут к кон­ троллеру без жесткого кодирования имени класса. В табл. 15.10 описан эффект от наличия атрибута в листинге 15.36. похоже на использование операции Таблица 15.10. Маршруты для контроллера Customer Маршрут Описание /Customer/List Этот URL нацелен на метод действия Li st () /Customer/MyAction Этот URL нацелен на метод действия Index () Создание более сложного маршрута Атрибут Route можно также применять к классу контроллера, позволяя опреде­ лять структуру маршрута, как показано в листинге Листинг 15.37. 15.37. Применение атрибута Rou te к контроллеру в файле CustomerController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers [Route("app/[controller]/actions/[action]/{id?}")] puЫic class CustomerController : Controller { ViewResult Index() => View("Result", new Resul t { Controller = nameof(CustomerController), Action = nameof(Index) puЫic ) ) ; ViewResult List (string id) { Result r = new Result { Controller = nameof(HomeController), Action = nameof(List), puЬlic }; r.Data["Id"J = id ?? "<no value>"; r.Data["catchall"] = RouteData.Values["catchall"J; return View{"Result", r); В маршруте определена смесь статических и переменных сегментов, а также используются маркеры [controller] и контроллера и методов действий. В табл. маршрута. [action] для ссылки на имена 15. 11 описан эффект от создания класса такого Глава 15. Маршрутизация URL Таблица 475 15.11. Маршруты для контроллера Customer Маршрут Описание app/custorner/actions/index Этот URL нацелен на метод действия Index () app/custorner/actions/index/rnyid Этот URL нацелен на метод действия Index () с необязательным сегментом ным в id, установлен­ rnyid app/custorner/actions/list Этот URL нацелен на метод действия List () app/custorner/actions/list/rnyid Этот URL нацелен на метод действия List () с необязательным сегментом ным в id, установлен­ rnyid Применение ограничений к маршрутам Маршруты, определенные посредством атрибутов, могут ограничиваться подобно маршрутам, которые определены в файле Startup. cs, с помощью того же самого встроенного подхода, используемого для маршрутов на основе соглашений. В листин­ ге созданное ранее в главе специальное ограничение применяется к необяза­ 15.38 тельному сегменту Листинг 15.38. id, определенному в атрибуте Ограничение маршрута в файле из папки Route. CustomerController. cs Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Models; namespace UrlsAndRoutes.Controllers [Route("app/[controller]/actions/[action]/{id:week day?}")] class CustomerController : Controller { puЬlic ViewResult Index() => View("Result", new Result { Controller = nameof(CustomerController), Action = nameof(Index) puЫic }) ; ViewResult List(string id) { Result r = new Result { Controller = nameof(HomeController), Action = nameof(List), puЫic }; r.Data["Id"] = id ?? "<no value>"; r.Data["catchall"] = RouteData.Values["catchall"]; return View("Result", r); Можно использовать все ограничения, описанные в табл. монстрировано в листинге 15.38, 15.8, или, как проде­ применять специальные ограничения, которые были зарегистрированы с помощью службы RouteOptions. Чтобы применить сразу несколько ограничений. их понадобится соединить в цепочку. отделяя друг от друга двоеточиями. 476 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Резюме В главе подробно обсуждалась система маршрутизации. Вы увидели, как опреде­ ляются маршруты по соглашениям и с помощью атрибутов. каким образом сопостав­ ляются и обрабатываются входящие URL. и как настраиваются маршруты за счет изменения способа. которым они сопоставляются с сегментами URL. а также за счет использования стандартных значений и необязательных сегментов. Кроме того, было показано. каким образом ограничивать маршруты, чтобы сузить диапазон запросов. для которых они будут давать совпадение, с применением встроенных ограничений и специальных классов ограничений. В следующей главе мы рассмотрим генерацию исходящих URL на основе маршру­ тов внутри представлений и выясним, как использовать средство областей. которое полагается на систему маршрутизации и может применяться для управления круп­ ными и сложными приложениями MVC. ГЛАВА 16 Дополнительные возможности маршрутизации как использовать систему маршрутизации в предыдущей главе было показано, URL, но это только часть общей картины. Мы также для обработки входящих должны быть в состоянии применять схему URL для генерации исходящих URL, ко­ торые могут встраиваться в представления. чтобы пользователи имели возможность щелкать на ссылках и отправлять формы обратно приложению, нацеливаясь на кор­ ректный контроллер и действие. В настоящей главе будут рассматриваться разнообразные приемы для генерации исходящих URL. демонстрироваться способы настройки системы маршрутизации за счет замены стандартных классов, реализующих маршрутизацию ся, как использовать средство областей сложное приложение MVC ветов по применению схем MVC, MVC, и объяснять­ которое позволяет разбивать крупное и на управляемые фрагменты. Dlaвa завершится рядом со­ URL в приложениях MVC. В табл. 16.1 приведена сводка. позволяющая поместить дополнительные возможности маршрутизации в контекст. Таблица 16. 1. Помещение дополнительных возможностей маршрутизации в контекст Вопрос Ответ Что это такое? Система маршрутизации предоставляет возможности, которые выходят за рамки сопоставления с НПР­ URL для запросов. Имеется также поддержка для генерации URL в представлениях, замещения встроенной функциональности маршрутизации специальными классами и структуризации приложения в виде изолированных разделов Чем они полезны? Каждое средство полезно по своим соображениям. Наличие возможности генерирования URL облегчает из­ необходимости в обновлении всех представлений. Существование возможности использова­ менение схемы URL без ния специальных классов позволяет подстраивать систему маршрутизации под собственные требования. Наличие возможности структуризации приложения упрощает пост­ роение сложных проектов Как они используются? Ищите детальные сведения в разделах главы Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 478 Окончание табл. Вопрос 16. 1 Ответ Существуют ли какие-то скры­ Конфигурация маршрутизации для сложного приложения тые ловушки или ограничения? может стать трудной в управлении Существуют ли альтернативы? Нет. Система маршрутизации ASP.NEТ В табл. Таблица 16.2 приведена сводка 16.2. Сводка по главе Задача - неотъемлемая часть Core по главе. Решение Листинг Генерирование якорного Используйте атрибуты элемента с и URL asp-action asp-controller Предоставление значений для Применяйте атрибуты с префиксом сегментов маршрутизации аsр-rоutе- Генерирование полностью Используйте атрибуты заданных asp-host URL Выбор маршрута для генера­ ции и asp-protocol, asp-fragment Применяйте атрибут asp-route в представлении или в методе действия 16.9, 16.10 16.11, 16.12 16.13 URL Генерирование URL без Используйте вспомогательный метод НТМL-элемента Настройка системы Применяйте метод маршрутизации в классе Создание специального Реализуйте интерфейс Url .Action () Configure () Startup IRouter 16.1416.21 Создайте области и используйте атрибут Area 16.2216.28 класса маршрутизации Разбиение приложения на функциональные разделы 16.216.5 16.6, 16.7 16.8 Подготовка проекта для примера Мы продолжим работать с проектом UrlsAndRoutes из предыдущей Startup, rде вызов Единственное изменение потребуется внести в класс UseMvcWithDefaultRoute () заменяется явным маршрутом с тем же самым резуль­ татом, как показано в листинге 16. l. Листинг 1б.1. Изменение конфигурации маршрутизации в файле из папки using using using using using using using using using using using главы. метода UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencylnjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; s tartup. cs Глава 16. Дополнительные возможности маршрутизации 479 n ame spa ce UrlsAndRoutes { p uЫ i c c la ss Startup { vo id Co nfigureSer v i ces(ISe rviceColl ection services ) { services.Configure<RouteOp tio ns>(options => o p tions.Const ra i ntMap.Add ( "weekday", type o f(WeekDayConstraint) ) ) ; services.AddMvc(); puЫic void Configure(IApplicat ionBui lder app.U seDevel o perEx ceptionPage() ; app.UseStatusCodePages(); app.Us eSta ti c file s() ; арр. Useмvc (routes => { puЫic арр, IHost ing Env i ronment env) { routes.мapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }) ; После запуска приложения браузер запросит стандартный правлен на действие Index контроллера D Routing Home (рис . URL, который будет на­ 16. \ ). х ф localhost ~41\3<> С Controller: HomeController Action: lndex Рис. 16.1. Выполнение примера приложения Генерирование исходящих Почти в каждом приложении MVC URL в представлениях пользователи должны располагать возможнос ­ тью перехода от одного представления к друтому, что обычно предполагает включение в первое представление ссылки. нацеленной на метод действия. который генерирует второе представление. Было бы заманчиво просто добавить статический элемент а (известный как якорный элемент), указав в его атрибуте href нужный метод дейс ­ твия. например: <а h ref= "/Home/CustomVariaЬle" >T his i s an outgo in g URL< / a> Предпол а га я . что в приложении применяется станда ртная конфигурация марш ­ рутизации, такой НТМL-элемент создает ссылку, которая будет нацелена на метод действия CustomVariaЫe () в контроллере Home. Вручную определяемые URL подоб­ ного рода создавать быстро и просто. Но вместе с тем они чрезвычайно опасны: из­ менив схему URL. URL для приложения. вы нарушите работу всех жестко закодированных Затем придется пройти с ь по все м представлениям в приложении и обновить все сс ылки на контроллеры и методы действий - утомительный , чреватый ошибками и Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 480 сложный в тестировании процесс. Более удачная альтернатива предусматривает ис­ пользование для генерации исходящих вает применение схемы URL рый гарантирует учет схемы URL системы маршрутизации. что обеспечи­ для динамического формирования URL URL способом. кото­ приложения. Генерирование исходящих ссылок Сгенерировать исходящий URL в представлении проще всего с использованием вспомогательной функции дескриптора якоря, которая создаст атрибут элементе а, как иллюстрируется в листинге представлению 16.2, href в НТМL­ в котором приведено добавление к /Views/Shared/Resul t. csh tml. Совет. Работа вспомогательных функций дескрипторов объясняется в главе 23. Листинг 1б.2. Использование вспомогательной функции дескриптора якоря в файле Resul t. cshtml из папки Views/Shared @model Result @{ Layout = null; < 1 DOCTYPE html> <l1tml> <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="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЬle-striped taЬle-sm" > <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аЫе> <а asp-action="CustomVariaЬle">This is an outgoing URL</a> </body> </html> Атрибут asp-action применяется для указания имени метода действия. на кото­ рый должен быть нацелен но увидеть на рис. URL в атрибуте href. Результат запуска приложения мож ­ 16.2. х с Controller: Action: This is an ootgoing URL Рис. 16.2. Использование вспомогательной функции дескриптора для генерации ссылки Глава 16. Дополнительные возможности маршрутизации Вспомогательная функция дескриптора устанавливает атрибут hre f 481 элемента а с применением текущей конфигурации маршрутизации. Просмотрев НТМL-разметку. отправленную браузеру. легко заметить, что она содержит следующий элемент: <а href="/Home/CustomVariaЬle">This is an outgoing URL</a> Может показаться, что для воссоздания вручную определенного URL, который был приведен ранее, пришлось затратить слишком много дополнительных усилий, но пре­ имущество данного подхода в том, что он автоматически реагирует на изменения в конфигурации маршрутизации. Чтобы продемонстрировать сказанное, добавим в файл Startup. cs Листинг 16.3. новый маршрут (листинг 16.3). Добавление нового маршрута в файле из папки s tartup. cs UrlsAndRoutes using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; 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))); services.AddMvc(); void Configure(IApplicationBuilder app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { puЬlic арр, IHostingEnvironment env) { routes.МapRoute( name: "NewRoute", template: "App/Do{action}", def aul ts: new { controller = "Home" } ) ; routes.MapRoute( name: "default", template: "(controller=Home)/{action=Index)/{id?)"); )) ; Новый маршрут изменяет схему лер Home. URL для запросов, которые нацелены на контрол­ Запустив приложение. можно заметить, что это изменение отразилось в НТМL-разметке, сгенерированной вспомогательной функцией дескриптора: <а href="/App/DoCustomVariaЫe">This is an outgoing URL</a> 482 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 Генерирование ссылок с использованием вспомогательной функции дескриптора решает важную проблему сопровождения. Мы имеем возможность изменять схему маршрутизации. а исходящие ссылки в представлениях автоматически отразят из­ менения без необходимости вручную редактировать представления в приложении. Когда производится щелчок на ссылке, исходящий URL применяется для создания входящего НТТР-запроса, и тот же самый маршрут затем используется для нацелива­ ния на метод действия и контроллер , которые будут обрабатывать запрос (рис. D Routing 16.3). х Controller: HomeController Actlon: CustomVariaЬle d: Рис. 1б.З. Результатом щелчка на ссылке является применение исходящего URL для создания входящего запроса Сопоставление исходящих URL с маршрутами Вы уже видели, что изменение маршрутов, определяющих схему нерации исходящих URL. URL, изменяет способ ге­ В приложениях обычно определено множество маршрутов, поэто­ му важно понимать, как они выбираются для генерации URL. Система маршрутизации обра­ батывает маршруты в порядке их определения, и каждый маршрут проверяется по очереди на предмет соответствия, что требует удовлетворения следующих трех условий. • Для любой переменной сегмента, определенной в шаблоне URL, должно быть доступно значение. Чтобы найти значения для каждой переменной сегмента, система маршрути­ зации просматривает сначала предоставленные (посредством свойств анонимного типа) значения, затем значения переменных для текущего запроса и, наконец, стандартные значения, определенные в маршруте . (Позже в главе мы еще возвратимся ко второму ис­ точнику значений из числа упомянутых . ) • Ни одно из значений, предоставленных для переменных сегментов, не должно конфлик­ товать с определенными в маршруте переменными, которые имеют только стандартные значения. Это переменные, для которых были предоставлены стандартные значения, но которые не встречаются в шаблоне шрута myVar URL. Например, в показанном ниже определении мар­ является переменной, имеющей только стандартное значение : routes. MapRo ute ( "MyRou te", " ( c o ntroll e r} / { a c t i o n) ", n e w ( myV a.r = "true " ) ) ; Чтобы соответствовать такому маршруту, значение для переменной myVar либо не долж­ но предоставляться, либо должно совпадать со стандартным значением . • Значения для всех переменных сегментов должны удовлетворять ограничениям маршру­ та. Примеры разных видов ограничений приводились в разделе "Ограничение маршрутов " предыдущей главы. Следует уяснить, что система маршрутизации не пытается найти маршрут, который дает наилучшее совпадение. Она находит только первое совпадение и использует данный мар­ шрут для генерирования URL; любые дальнейшие маршруты игнорируются . По указанной Глава 16. Доnолнительные возможности маршрутизации 483 причине наиболее специфичные маршруты должны определяться первыми. Важно прове­ рить генерацию исходящих URL. Попытка сгенерировать URL, для которого не удается найти соответствующий маршрут, приведет к созданию ссылки с пустым атрибутом href: href="">This is an outgoing URL</a> <а Такая ссылка корректно визуализируется в представлении, но не будет функционировать ожидаемым образом, когда пользователь выполнит на ней щелчок. Если вы генерируете только null, URL (как будет показано позже в главе), тогда результирующим значением будет которое визуализируется в виде пустой строки в представлениях. С помощью име­ нованных маршрутов можно получить определенный контроль над сопоставлением маршру­ тов. За более подробными сведениями обратитесь в раздел "Генерирование URL из специ­ фического маршрута" далее в главе. Направление на другие контроллеры В случае указания атрибута asp-action внутри элемента а вспомогательная фун­ кция дескриптора предполагает, что нужно установить в качестве цели действие в том же контроллере, который вызвал визуализацию представления. Чтобы создать исходящий бут URL, который направляет на другой контроллер, можно применить атри­ asp-controller Листинг 16.4. (листинг 16.4). Направление на другой контроллер в файле из папки Result.cshtml Views/Shared @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="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЫe-sm"> <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аЫе> asp-controller="Admin" asp-action="Index"> This targets another controller <а </а> </body> </html> В результате визуализации представления сгенерируется следующая НТМL­ разметка: <а href="/Admin">This targets another controller</a> Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 484 Запрос к URL, который нацелен на метод действия Index () контроллера Admin. / Admin. Системе маршру­ был выражен вспомогательной функцией дескриптора как тизации известно, что маршрут, определенный в приложении. по умолчанию будет использовать метод действия Index ().позволяя опускать ненужные сегменты. При выяснении, каким образом нацеливаться на заданный метод действия, сис­ тема маршрутизации включает маршруты, которые были определены с применени­ ем атрибута Index Route. В листинге 16.5 атрибут asp-controller нацелен на действие Customer. к которому был применен атрибут Route в главе 15. в контроллере Листинг 16.5. Направление на действие, декорированное атрибутом в файле из папки Resul t. cshtml Route, Views/Shared @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Ьle-striped taЬle-sm"> <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аЫе> <а asp-controller="Customer" asp-action="Index">This is an outgoing URL</a> </body> </html> Вот какая ссылка сгенерируется: <а href="/app/Customer/actions/Index">This is an outgoing lJRL</a> Результат соответствует атрибуту Customer в главе Rou te. который применялся к контроллеру 15: [Route("app/[controller]/actions/[action]/{id:weekday?}")] class CustomerController : Controller { puЫic Передача дополнительных значений Системе маршрутизации можно передавать значения для переменных сегментов за счет определения атрибутов с именами, начинающимися со строки за которой следует имя сегмента. Таким образом, атрибут ся для установки значения сегмента id (листинг 16.6). asp-route-. asp-1·oute-id исполr,зует­ Глава 16. Доnолнительные возможности маршрутизации Листинг 16.6. 485 Предоставление значений для переменных сегментов в файле Resul t. cshtml из папки Views/Shared @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="m-1 р-1"> <tаЫе class="taЫe taЬle-bordered taЬle-striped taЫe-sm"> <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аЫе> asp-controller="Home" asp-action="Index" asp-route-id="Hello"> This is an outqoinq URL <а </а> </body> </html> Здесь предоставляется значение для переменной сегмента по имени ложение применяет маршрут, показанный в листинге 16.3, id. Если при­ тогда при визуализации представления будет получена следующая НТМL-разметка: <а href="/App/Doindex?id=Hello">This is an outgoing URL</a> Обратите внимание, что значение сегмента было добавлено как часть строки за­ проса, чтобы вписываться в шаблон URL, определенный маршрутом. Причина свя­ зана с отсутствием переменной сегмента, которая бы соответствовала маршруте. Чтобы решить проблему, необходимо отредактировать файл оставив только маршрут. который имеет сегмент Листинг 16. 7. Редактирование маршрутов в файле из папки using using using using using using using using using using using id (листинг 16. 7). s tartup. cs UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; i d в этом Startup. cs, Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 486 namespace UrlsAndRoutes ( puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.Configure<RouteOptions>(options => options.ConstraintMap.Add("weekday", typeof(WeekDayConstraint))); services.AddMvc(); void Configure(IApplicationBuilder арр, IHostingEnvironment env) ( app.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { / / rou tes . МapRou te ( 11 name: "NewRoute", 11 template: "App/Do{action}", 11 defaul ts: new { controller = "Home" } ) ; routes.MapRoute( name: "default", template: "(controller=Home}/(action=Index}/(id?}"); puЬlic }) ; Запустив приложение еще раз, можно заметить, что вспомогательная функция де­ скриптора выдала следующий НТМL-элемент. в котором значение свойства чено как сегмент <а id вклю­ URL: href="/Home/Index/Hello">This is an outgoing URL</a> Повторное использование переменных сегментов При описании способа сопоставления маршрутов для исходящих URL было указано, что когда система маршрутизации пытается найти значения для каждой переменной сегмента в шаблоне URL маршрута, она просматривает текущий запрос. Такое поведение сбивает с толку многих программистов и может вылиться в длительный сеанс отладки. Предположим, что приложение имеет единственный маршрут: app.UseMvc(routes => ( routes.MapRoute(name: "MyRoute", template: "(controller}/(action}/(color}/(page}"); }) ; Пусть в текущий момент пользователь запросил URL вида /Home/Index/Red/100 и ви­ зуализируется показанная ниже ссылка: asp-controller="Home" asp-action="Index" asp-route-page="789"> This is an outgoing URL <а </а> Можно было бы ожидать, что система маршрутизации не сумеет выполнить сопоставление с маршрутом, т.к. не было предоставлено значение для переменной сегмента color, к тому же для нее не определено стандартное значение. Однако дело обстоит не так. Система мар- Глава 16. Дополнительные возможности маршрутизации 487 шрутизации будет сопоставлять с определенным ранее маршрутом. В результате сгенери­ руется следующая НТМL-разметка: <а href="/Home/Index/Red/789">This is an outgoing URL</a> Система маршрутизации достаточно интеллектуальна, чтобы провести сопоставление с мар­ шрутом, поскольку при генерировании исходящего URL она будет повторно использовать URL. В рассматриваемом случае переменная включал URL, с которого стартовал пользователь. значения переменных сегментов из входящего color получит значение Red, т.к. его Это не поведение, предназначенное для крайних случаев. Система маршрутизации будет применять такой прием как часть обычной оценки маршрутов, даже если имеется последу­ ющий маршрут, который обеспечит совпадение, не требуя повторного использования зна­ чений из текущего запроса. Я настоятельно рекомендую не полагаться на описанное поведение и предоставлять значе­ ния для всех переменных сегментов в шаблоне URL. Если опираться на данное поведение, то код станет гораздо труднее в восприятии, к тому же в коде будут делаться допущения относительно порядка, в котором пользователи выдают запросы, что обернется проблема­ ми при сопровождении приложения. Генерирование полностью заданных URL Все ссылки, которые генерировались до сих пор, содержали относительные URL. но вспомогательная функция дескриптора а способна также генерировать полностью заданные Листинг URL, 16.8. как показано в листинге 16.8. Генерирование полностью заданного из папки URL в файле Resul t. cshtml Views/Shared @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="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЬle-sm"> <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аЫе> 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> Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 488 Атрибуты asp-protocol, asp-host и asp-fragment применяются для указания протокола (https в листинге). имени сервера (myserver .mydomain. com) и фрагмен­ та URL (myFragment). Эти значения объединяются с выводом из системы маршру­ тизации для создания полностью заданного URL. который можно увидеть, запустив приложение и просмотрев отправленную браузеру НТМL-разметку: <а href="https://myserver.mydomain.com/Home/Index/Hello#myFragment"> This is an outgoing URL </а> Будьте осторожны при использовании полностью заданных URL, поскольку они со­ здают зависимости от инфраструктуры приложения и, когда инфраструктура изменяет­ ся. вы должны не забыть внести соответствующие изменения в представления МVС. Генерирование URL из специфического маршрута В предшествующих примерах система маршрутизации выбирала маршрут для применения при генерировании URL. Если важно генерировать URL в специфичес­ ком формате, тогда можно указать маршрут. подлежащий использованию для гене­ рации исходящего Startup. cs (листинг Листинг using using using using using using using using using using using URL. Чтобы продемонстрировать прием в работе. добавим в файл новый маршрут. так что в примере приложения окажется два маршрута 16.9). 16.9. Добавление маршрута в файле Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes { puЬlic 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.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { арр, IHostingEnvironment env) { 11 routes.MapRoute( name: "NewRoute", 11 template: "App/Do{action)", 11 11 defaults: new { controller = "Home" }) ; Глава 16. Доnолнительные возможности маршрутизации 489 routes.MapRoute( nаще: "default", template: "{controller=Home}/{action=Index)/{id?}"); routes.мapRoute( name: "out", template: "outЬound/(controller=Home}/(action=Index}"); 1); Представление, приведенное в листинге 16.1 О, содержит два якорных элемен­ та, каждый из которых указывает на те же самые контроллер и действие. Разница в том, что второй элемент применяет атрибут asp-route вспомогательной функции дескриптора для указания на необходимость использования маршрута рации URL. Листинг относящегося к атрибуту 16.10. Генерирование URL ou t при гене­ href. в файле Result.cshtml из папки Views/Shared @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="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЬle-sm"> <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аЫе> <а asp-controller="Home" asp-action="CustomVariaЬle">Тhis is an outqoinq tJRL </а> <а asp-route="out">This is an outqoinq URL</a> </body> </html> asp-route может применяться только в ситуации, asp-controller и asp-action, т.е. выбирать можно Атрибут атрибуты когда отсутствуют лишь специфичес­ кий маршрут для контроллера и действия. которые привели к визуализации пред­ ставления. В результате запуска приложения и запрашивания CustomVariaЫe получаются два разных URL, is an outgoing URL</a> href="/outbound">This is an outgoing URL</a> <а href="/Home/CustomVariaЫe">This <а URL вида / Home / сгенерированные маршрутами: 490 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Доводы против именованных маршрутов Проблема с зависимостью от имен маршрутов при генерации исходящих URL связана с тем, что это нарушает принцип разделения обязанностей, который занимает центральное место в паттерне проектирования MVC. При генерации ссылки либо URL в представлении или ме­ тоде действия необходимо сосредоточиться на действии и контроллере, куда пользователь будет направлен, а не на формате URL, который будет использоваться. Привнося знание о различных маршрутах в представления или контроллеры, мы создаем зависимости, которых можно было бы избежать. В своих проектах я стараюсь не именовать маршруты (указывая null в аргументе name) и предпочитаю применять комментарии в коде, чтобы не забыть о том, для чего предназначен каждый маршрут. Генерирование URL (без ссылок) Ограничение вспомогательных функций дескрипторов заключается в том, что они трансформируют НТМL-элементы и не могут без труда быть переориентированы в случае, если необходимо генерировать URL для приложения без окружающей НТМL­ разметки. MVC предлагает вспомогательный класс. который можно исполь­ URL напрямую. доступный через метод Url .Action (),как де­ листинге 16. l 1. Инфраструктура зовать для создания монстрируется в Листинг 16.11. Генерирование URL в файле Resul t. cshtml из папки Views/ Shared @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="m-1 р-1"> <tаЫе class="taЫe taЬle-bordered taЬle-striped taЬle-sm"> <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аЫе> <p>URL: </body> </html> @Url.Action("CustomVariaЬle", В аргументах метода Ur 1. Action () "Ноте", new { id = 100 })</р> указываются метод действия. контроллер и значения для любых переменных сегментов. Результатом добавления разметки в лис­ тинге 16.11 <p>URL: будет генерация следующего вывода: /Home/CustomVariaЫe/100</p> Глава 16. Дополнительные возможности маршрут изации Генерирование URL в Ur l . Ac ti o n () URL внутри кода С#. Метод ния контроллера Листинг Home . 491 методах действий может применяться также и в методах дей ствий для созда­ В листинге 16. 12 модифицирован один из методов действий URL с использованием Ur 1 . Act i o n ( ) . чтобы генерировать 16.12. Генерирование URL внутри метода действия в файле HomeController. cs из папки Controllers u si ng Mi c r oso ft.AspNet Cor e.Mv c; u si n g UrlsAndRo utes. Models ; name spa ce Ur lsAn dRo u tes . Contr o l l e r s pu Ыi c clas s Home Co n tro ll e r : Co n t r o ller ViewRe s u lt Ind e x( ) => Vi e w ( "Re su l t ", Resul t . { p u Ы ic пеw Co п tr o l l e r = Ac t io n = пameof(Home C oп tro l ler ) , na meof(I nde x ) }) ; Vi ewResul t C u sto m VariaЬle (str i ng id ) { Resul t r = n e w Result { Co ntr o l le r = п am e o f ( H o m eCoпt r o ller) , p uЬl ic A c t io п = пame o f (C u s t omV a r i a Ьl e) , }; r.Data["Id"J = id ? '? " < п о v a l u e> "; r.Data["Url"] = Url.Action("CustomVariaЬle", "Home", new { id = 100 }) ; ret urn Vie w ( "Resu l t ", r ) ; Запустив приложение и запросив URL вида / H ome / Cu s t omVari aЫe . л егко заме ­ тить. что в таблице появилась строка. которая отображает данный С <D loca!host 60588/Horne/C~rstonNariaЫe Coпtroller: HomeController Action: CustomVariaЫe id : <novalue> 1/Home/CustomVariaЬle/100 url : URL: /Нome/CustomVariaЬle/100 Рис. 16.4. Генерирование URL в методе действия URL (ри с . 16.4). 492 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 Настройка системы маршрутизации Вы уже видели, насколько гибкой и конфигурируемой является система маршру­ тизации, но если она не удовлетворяет существующим требованиям. тогда ее поведе­ ние можно настроить. В этом разделе будут показаны два способа такой настройки. Изменение конфигурации системы маршрутизации В главе 15выузнали, как конфигурировать объект RouteOptions в файле Startup. cs RouteOptions так­ для настройки специального ограничения маршрута. Объект же применяется для конфигурирования ряда средств маршрутизации с помощью свойств, описанных в табл. Таблица 16.3. 16.3. Конфигурационные свойства RouteOptions Описание Имя AppendTrailingSlash Когда это свойство типа bool системой маршрутизации равно true, к генерируемым URL добавляется завершающий символ косой черты. Стандартным значением является LowercaseUrls Когда это свойство типа bool равно true, false результирующие URL преобразуются в нижний регистр, если контроллер, дейс­ твие или значения сегментов содержат символы нижнего ре­ гистра. Стандартным значением является В листинге 16.13 приведено содержимое файла false Startup.cs с добавленными операторами для настройки обоих конфигурационных свойств из табл. Листинг 16.13. Конфигурирование системы маршрутизации в файле из папки using using using using using using using using using using using 16.3. Startup. cs UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; UrlsAndRoutes.Infrastructure; namespace UrlsAndRoutes { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.Configure<RouteOptions>(options => { options.Constraintмap.Add("weekday", typeof(WeekDayConstraint)); options.LowercaseUrls = true; options.AppendTrailingSlash = true; }) ; services.AddMvc(); Глава 16. Дополнительные возможности маршрутизации 493 void Configure (IApp1icationBuilder арр, IHostingEnvironment env) { app.UseDeveloperExceptio nPage(); app.UseStatusCodePages( ) ; app.UseStaticfiles(); app.UseMvc ( routes => { routes.MapRoute( name: "default", template: "{ contro ller=Home}/{act i o n=Inde x )/(id?}" ) ; routes.MapRoute( puЫic name: "out ", template: "outbound / (cont roller =Home) /{ action=Index }" ) ; }) ; После запуска приложения и просмотра URL. которые сгенерировала система мар­ шрутизации, выяснится. что изменение конфигурационных свойств обеспечило пре­ образование (рис. URL в нижний регистр и добавление к ним завершающей косой черты 16.5). Controller: HomeController Aclion: CustomVarlaЫe id: <ПО url : ihome/customvarlaЫe/100/ value> URL · /home/customvariaЬle/1001 Рис. 16.5. Конфигурирование системы маршрутизации Соэдание специального класса маршрута Если вам не нравится способ, которым система маршрутизации производит сопос­ тавление с URL. или для вашего приложения необходимо реализовать что -то специфи­ ческое, тогда можете создать собственные классы маршрутизации и использовать их для обработки URL. Инфраструктура AspNetCore. Rout i ng. IRo uter. ASP.NET предоставляет интерфейс Microsof t. который можно реализовать для создания специаль­ ного маршрута. Вот как выглядит определение интерфейса IRout e r: using Syste m.Threading.Tasks; name spa ce Mi c roso f t.AspNetCo re . Routing puЫic inte rface IRouter ( Task Rou teAsy nc( Route Context con t ext ) ; VirtualPathData GetVirtual Path(Virtua lPa thContext c ont ex t) ; Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC 2 494 Чтобы создать RouteAsync () специальный маршрут, понадобится для обработки входящих запросов и метод нужно генерировать исходящие реализовать метод GetVirtualPath (). если URL. В целях демонстрации мы создадим специальный класс маршрутизации, который будет обрабатывать запросы к унаследованным какое-то существующее приложение в старые URL MVC. URL. Предположим, что мы перенесли но есть пользователи. которые поместили в закладки или жестко закодировали их в сценариях. Мы хотим по-пре­ жнему поддерживать такие старые URL. Мы могли бы обработать это с применением обычной системы маршрутизации, но решение данной задачи будет хорошим приме­ ром для целей настоящего раздела. Маршрутизация входящих URL Чтобы понять, как работают специальные маршруты, мы начнем с создания одно­ го такого маршрута, который будет обрабатывать каждый аспект запроса самостоя­ тельно, не используя контроллер и представление. Создадим в папке файл класса по имени денной в листинге Листинг using using using using using using 16.14. LegacyRoute. cs с реализацией интерфейса Infrastructure IRouter, приве­ 16.14. Содержимое файла LegacyRou te. cs из папки Infras tructure Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Routing; System; System.Linq; System.Text; System.Threading.Tasks; namespace UrlsAndRoutes.Infrastructure puЫic class LegacyRoute : IRouter { private string(] urls; puЬlic LegacyRoute(params string(] targetUrls) { this.urls = targetUrls; puЫic Task RouteAsync(RouteContext context) { string requestedUrl = context.HttpContext.Request.Path .Value.TrimEnd('/'); if (urls.Contains(requestedUrl, StringComparer.OrdinalignoreCase)) context.Handler = async ctx => { HttpResponse response = ctx.Response; byte[] bytes = Encoding.ASCII.GetBytes($"URL: {requestedUrll"); await response.Body.WriteAsync(bytes, О, bytes.Length); }; return Task.CompletedTask; puЬlic VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; 495 Глава 16. Дополнительные возможности маршрутизации LecagyRoute реализует Rou teAs ync ( ) , который Класс метода интерфейс IRouter, вскоре мы добавим поддержку и для исходящих В методе RouteAsync () но определяет код только для применяется для обработки входящих запросов; URL. находится совсем немного операторов, но в своей работе они полагаются на несколько важных типов ASP.NEТ. Лучше всего начать исследова­ ние с сигнатуры метода: puЫic Метод async Task RouteAsync(RouteContext context) RouteAsync () отвечает за оценку. может ли запрос быть обработан, и если может, то за управление процессом, посредством которого генерируется ответ, отправ­ ляемый обратно клиенту. Такой процесс выполняется асинхронно, поэтому метод RouteAsync () возвращает объект Task. Метод RouteAsync () вызывается с аргументом RouteContext, который предо­ ставляет доступ ко всему, что известно о запросе, и предлагает средства, требуемые для отправки ответа клиенту. Класс Microsoft. AspNetCore. Routing Таблица RouteContext находится в пространстве имен и определяет три свойства, описанные в табл. 16.4. 1б.4. Свойства, определенные в К11ассе RouteContext Описание Имя RouteData Это свойство возвращает объект RouteData. на средства Microsoft. AspNetCore. Routing. При написании специального маршрута, который полагается MVC (как объясняется в следующем разделе), данный объект используется для определения контроллера, метода действия и аргументов, применяемых для обработки запроса HttpContext Это свойство возвращает объект Microsoft .AspNetCore. Http. HttpContext, который предоставляет доступ к деталям НТТР-запроса и предназначен для выпуска НТТР-ответа Handler Это свойство используется для предоставления системе маршрутизации RequestDelegate, который будет обрабатывать запрос. Если ме­ RouteAsync не устанавливает данное свойство, тогда система маршру­ объекта тод тизации продолжит работу своим способом посредством набора маршрутов в конфигурации приложения Система маршрутизации вызывает метод RouteAsync () каждого маршрута в при­ ложении и после каждого вызова проверяет значение свойства Handler было установлено в RequestDelegate, Handler. Если свойство тогда маршрут предоставляет систе­ ме маршрутизации делегат, который способен обработать запрос, и этот делегат вызы­ вается для генерации ответа. Ниже показана сигнатура делегата определенного в пространстве имен RequestDelegate, Microsoft. AspNetCore. Http: using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http { puЫic delegate Task RequestDelegate(HttpContext context); Делегат Task, RequestDelegate принимает объект HttpContext и возвращает объект Handler не установлено ни в который будет генерировать ответ. Если свойство одном из маршрутов, то системе маршрутизации известно, что приложение не может обработать запрос, и она сгенерирует ответ 404 - Not Found (404 - не найдено). 496 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 С этой целью реализация метода RouteAsync () должна выяснить, может ли она обработать запрос, для чего обычно требуется объект HttpContext. В рассматрива­ HttpContext. Request, которое возвращает объект Microsoft.AspNetCore.Http.HttpRequest, описывающий запрос. Объект HttpRequest предоставляет доступ ко всей информации о запросе, включая заголов­ емом примере применяется свойство ки. тело и детали того, откуда запрос произошел. но мы заинтересованы в свойстве Path. поскольку оно дает подробные сведения относительно ентом. Свойство Path URL. запрошенного кли­ PathString. предлагающий удобные мето­ URL. Мы используем свойство Value, т.к. оно возвращает объект ды для объединения и сравнения путей дает раздел полного пути из поддерживаемых URL. URL в виде строки, которую можно сравнить с набором полученным конструктором LegacyRoute. string requestedOrl = context.HttpContext.Request.Path.Value.TrimEnd('/'); if (urls.Contains(requestedUrl, StringComparer.OrdinalignoreCase)) { Здесь с помощью метода Tr imEnd ( ) из URL удаляется завершающая косая черта (если она есть), которая могла быть добавлена либо пользователем, либо в результате установки конфигурационного свойства AppendTrailingSlash, описанного в разде­ ле "'Изменение конфигурации системы маршрутизации" ранее в главе. Если запрошенный путь входит в число сконфигурированных для поддержки клас­ сом LegacyRoute, тогда мы устанавливаем свойство Handler с применением лямбда­ функции, которая будет генерировать ответ: context.Handler = async ctx => ( HttpResponse response = ctx.Response; byte[J bytes = Encoding.ASCII.GetBytes($"URL: {requestedOrll"); await response.Body.WriteAsync(bytes, О, bytes.Length); }; Свойство HttpContext. Response возвращает объект Ht tpResponse, который может использоваться для создания ответа клиенту и предоставляет доступ к заголов­ кам и содержимому, подлежащему отправке клиенту. Метод HttpResponse. Body. Wri teAsync () применяется для асинхронной записи в качестве ответа простой стро­ ки ASCII. Такой прием не будет предприниматься в реальном проекте, но он позволяет выпустить ответ, не выбирая и не визуализируя представления (хотя в следующем разделе будет показано, как это делать). Когда свойство Handler установлено, системе маршрутизации известно, что ее поиск маршрута закончен и можно вызывать делегат для генерации ответа клиенту. Применение специального класса маршрута Расширяющий метод MapRoute (). который до сих пор использовался для со­ здания маршрутов. не поддерживает применение специальных классов маршрутов. Чтобы воспользоваться классом (листинг Листинг LegacyRou te, придется задействовать другой подход 16.15). 16. 15. Применение специального класса маршрутизации в файле Startup. cs из папки using System; using System.Collections.Generic; UrlsAndRoutes Глава 16. Дополнительные возможности маршрутизации using using using using using using using using using 497 System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Bui lder; Microsoft.AspNetCore.Hos ting; Microsoft.AspNetCore.Http ; Microsoft.Extensions.Dep endencyinjection; Microsoft.AspNetCore.Ro uting.Constraints; Microsoft.AspNetCore.Rou ting; UrlsAndRoutes.Infrastruc ture; namespace UrlsAndRoutes { puЫic class Startup { void ConfigureServices(IServic eCollection services) { services.Configure<RouteO ptions>(options => { options.ConstraintMap.Add ("weekday", typeof(WeekDayConstraint) ); options.LowercaseUrls = true; options.AppendTrailingSla sh = true; puЫic }) ; services.AddMvc(); void Configure(IApplicationBuil der арр, IHostingEnvironment env) { app.UseDeveloperExceptio nPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { routes.Routes.Add(new LegacyRoute( "/articles/Windows 3.1 OVerview.html", "/old/.NET_l.O_Cla;s_Lib rary")); puЫic routes.MapRoute( name: "default", template: "{controller=Home}/{actio n=Index}/{id?}"); routes.MapRoute( name: "out", template: "outbound/{controller=Hom e}/{action=Index}"); }) ; Add ( ) на IRouter. В теку­ При использовании специальных классов потребуется вызвать метод коллекции маршрутов, чтобы зарегистрировать класс реализации щем примере аргументами конструктора URL, являются унаследованные LegacyRoute которые должен поддерживать специальный маршрут. Запустив приложение и запросив /articles/Windows _ 3. l _ Overview. html, специальный маршрут отобразит запрошенный Маршрутизация на контроллеры URL можно увидеть результат (рис. MVC Существует крупная брешь между сопоставлением с простыми строками применением системы контроллеров, действий и представлений туры MVC. - 16.6). Razor URL и инфраструк­ 498 Часть 11 . Подробные сведения об инфраструктуре ASP.NET Core MVC 2 С URL: Ф localhos /articles/Шndows_З.l_Overview.html Рис. 16.6. Использование специального маршрута К счастью, при создании специальных маршрутов реализовывать такую функцио­ нальность самостоятельно н е придется, потому что для выполнения в се й тяжелой р а ­ боты может быть задействов а н класс. который подготовиться к применению инфраструктуры файл класса по имени тинге LegacyCont r oller. c s MVC и с пользует "за кулисами". Что бы MVC . до бавим в папку Co n troll ers с содержимым . прив еденным в лис­ 16.16. Листинг 16.16. Содержимое файла LegacyController.cs из папки Controllers u s ing Mi c r oso ft.AspN etCor e.Mvc; n ames pa ce Ur l sAnd Routes . Cont r ollers p uЫi c c l ass LegacyContro ll e r : Control le r puЫi c Vie wResult GetL e gacyUr l(stri ng leg acyUrl) => View ( (o bject) l egac yUrl); Метод дей ствия Get LegacyU RL () в таком простом контроллер е принимает п а ­ раметр, который содержит унаследованный URL, запр о шенный клиентом. Если бы данный контр оллер был реализован в реальном проекте. то упомянутый метод и с ­ пользовался бы для извлечения запрошенных файлов . но здесь про сто отображает с я URL в представлении. Совет. Обратите внимание, что в листинге типу 16.16 аргумент метода ob ject . Одна из перегруженных версий метода Vie w () s t r ing, указывающий имя представления для визуализации, v i ew ( ) приводится к принимает параметр типа и без приведения компи­ лятор С# выберет именно ее . Во избежание этого мы выполняем приведение к object , чтобы однозначно вызывалась перегруженная версия, которая принимает модель пред­ ставления и применяет стандартное представление. Проблему можно было бы также ре­ шить за счет использования перегруженной версии, принимающей как имя представле­ ния, так и модель представления, но предпочтительнее по возможности не делать явных ассоциаций между методами действий и представлениями. За дополнительными сведе­ ниями обращайтесь в главу 17. Создадим папку View s/Le gacy и поме стим в нее файл представления по им е ни Ge tLegacyUrl. c shtml с содержимым из листинга 16.17. Новое пр едставление отоб­ ражает знач е ние модели. которое покажет запрошенный клиентом URL. Глава 16. Дополнительные возможности маршрутизации Листинг Содержимое файла 16.17. GetLegacyUrl. cshtml из папки 499 Views/Legacy @model string @{ 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="m-1 р-1"> <h2>GetLegacyURL</h2> The URL requested was: @Model </body> </html> В листинге им URL Листинг 16.18 класс LegacyRoute модифицирован так. чтобы обрабатываемые маршрутизировались на действие 16.18. GetLegacyUr 1 Маршрутизация на контроллер в файле из папки контроллера Legacy. LegacyRoute. cs Infrastructure using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.Dependencylnjection; namespace UrlsAndRoutes.Infrastructure { puЫic class LegacyRoute : IRouter { private string[] urls; private IRouter шvcRoute; puЬlic LeqacyRoute(IServiceProvider services, paraшs string[] targetUrls) this.urls = targetUrls; шvcRoute = services.GetRequiredService<МvcRouteHandler>(); } 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. Values [ "legacyUrl"] = requestedUrl; await шvcRoute.RouteAsync(context); puЬlic VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; puЬlic Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 500 Класс Microsoft. AspNetCore. Mvc. In ternal. MvcRou teHandler предоставляет controller и action при­ механизм, посредством которого переменные сегментов меняются для нахождения класса контроллера, выполнения метода действия и воз­ вращения результата клиенту. Этот класс был написан таким образом, что к нему можно обращаться из специальной реализации чения controller и action, IRouter. которая предоставляет зна­ а также любые другие требующиеся значения вроде ар­ гументов метода действия. В листинге 16.18 создается новый экземпляр класса MvcRouteHandler, которому делегируется задача нахождения класса контроллера. Для этого необходимо предо­ ставить данные маршрутизации: context.RouteData.Values["controller"J = "Legacy"; context. RouteData. Values [ "action"] = "GetLegacyUrl"; context.RouteData.Values["legacyUrl"J = requestedUrl; Свойство RouteContext. RouteData. Vales возвращает словарь. который исполь­ зуется для предоставления значений данных классу MvcRouteHandler. В стандарт­ ной системе маршрутизации значения данных создаются путем применения шаблона URL к запросу. но в специальном классе маршрута значения жестко закодированы. так что всегда производится направление на действие Legacy. GetLegacyUrl контроллера legacyUr l. которое Между запросами изменяется только значение данных устанавливается в URL запроса: оно будет применяться в качестве аргумента с таким же именем, получаемого методом действия. Последнее изменение в листинге 16.18 делегирует ответственность за поиск и ис­ пользование класса контроллера для обработки запроса. await mvcRoute.RouteAsync(context); Объект RouteCoritext, теперь содержащий значения coritroller. action и legacyUrl. передается методу RouteAsync () объекта MvcRouteHandler, который берет на себя обязанность за любую дальнейшую обработку запроса. включая уста­ новку свойства Handler. решении о том. какие В результате класс URL LegacyRoute может сосредоточиться на он будет обрабатывать. не имея дела с деталями работы с контроллерами напрямую. Объект MvcRouteHandler, выполняющий работу в рассматриваемом примере. должен запрашиваться как служба. что объясняется в главе конструктор LegacyRoute 18. Чтобы предоставить с объектом реализации обходим ему для создания объекта IServiceProvider. который не­ MvcRouteHandler. в классе Startup обновлен оператор, определяющий маршрут. с целью снабжения его возможностью доступа к службам приложения (листинг Листинг using using using using using using 16. 19). 16.19. Обеспечение доступа к службам приложения в файле Startup. cs из папки UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Глава 16. Дополнительны е возможности маршрутизации using using using using using 501 Microsoft.AspNetCore . Ht tp; Mi c ros oft.Ex tensi ons . Dependencylnjecti on ; Micros of t. Asp Ne tCore . Routing . Cons t raints; Microso ft . AspNetCore . Rout ing ; UrlsAndRoutes.Infrastruct ur e; namespace UrlsAndRoutes { puЫ i c c l ass Startup { void ConfigureServices ( IServiceCollection services ) { services . Co nfigure <RouteOptio ns >(opt i ons => { options . ConstraintMap . Add ( "weekday", t ypeo f (Week DayConstr aint )) ; options.LowercaseUrls = true ; options.AppendTrailingSlash = true ; puЫic )) ; se r vices . AddMv c() ; void Configure(IApplicat i onBui lder app.UseDeve lo perExcep t ionPa ge(); app.UseS tatusCodePag es(); app.UseSt atic fil es() ; app .Use Mvc(routes => ( puЫic арр, IHostingEnvironment env) ( routes.Routes.Add(new LegacyRoute( app.ApplicationServices , "/articles/Windows 3.1 Overview.html", "/old/.NET_l.O_Cla;s_Library")); rou te s . MapRou t.e ( name : " defaul t", template: " {controller= Home)/{action= I ndex)/ {id ?} " ) ; routes . MapRoute ( name : " out ", template: " outbound/{ cont roller=H ome) /{ a c tioп= In dex ) " ); }) ; Запу стив приложение и запрос и в / articles / Wi ndows 3 . 1 _ Overv i ew . h tml еще раз. можно увидеть. что простой текстовый ответ был заменен выводом из представ ­ ления (рис. 16. 7). С) Routing GetlegacyU RL The URL requested 111as: /articles/Windo1•1s_3. 1_0verview.htnil Рис. 16. 7. Делегирование работы с контроллерами и представлениями Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 502 Генерирование исходящих URL Для поддержки генерации исходящих LegacyRoute Листинг метод 16.20. GetVirtualPath Генерирование исходящих из папки using using using using using using using using URL мы должны реализовать в классе (),как показано в листинге URL в файле 16.20. LegacyRou te. cs Infrastructure Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Routing; System; System.Linq; System.Text; System.Threading.Tasks; Microsoft.AspNetCore.Mvc.Internal; Microsoft.Extensions.Dependencyinjection; namespace UrlsAndRoutes.Infrastructure puЫic class LegacyRoute : IRouter ( private string[J 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"J = "Legacy"; coлtext.RouteData.Values["action"J = "GetLegacyUrl"; context.RouteData.Values["legacyUrl"J = requestedUrl; await mvcRoute.RouteAsync(context); puЫic VirtualPathData GetVirtualPath(VirtualPathContext context) { if (context. Values. ContainsKey ( "legacyUrl")) { string url = context.Values["legacyUrl"] аз string; if (urls.Contains(url)) { return new VirtualPathData(this, url); return null; Система маршрутизации вызывает метод который был определен в классе дящий URL, объект VirtualPathContext, носительно Startup, GetVirtualPath () требующийся приложению. Аргумент метода URL. В табл. 16.5 каждого маршрута. давая каждому шанс сгенерировать исхо­ GetVirtualPath () - это который предоставляет необходимую информацию от­ описаны свойства класса VirtualPathContext. 503 Глава 16. Доnолнительные возможности маршрутизации 16.5. Свойства, определенные в классе VirtualPathContext Таблица Имя Описание RouteName Это свойство возвращает имя маршрута Values Это свойство возвращает индексированный по имени словарь значе­ ний, которые могут применяться для переменных сегментов ArnЬientValues Это свойство возвращает словарь значений, которые полезны при гене­ рации URL, но не будут встраиваться в результат. В случае реализации собственного класса маршрутизации такой словарь обычно пуст Это свойство возвращает объект HttpContext HttpContext, который предо­ ставляет информацию о запросе и ответе, находящемся в процессе подготовки В Va 1 u е s примере свойство рассматриваемом ния значения по имени legacyUrl, получе­ используется для и если оно соответствует одному из URL, ко­ торые были сконфигурированы для поддержки маршрутом, тогда возвращается объект VirtualPathData, снабжающий систему маршрутизации деталями Аргументами конструктора класса IRouter, который генерирует URL, VirtualPathData и сам URL. являются объект реализации URL. return new VirtualPathData(this, url); В листинге показано измененное представление 16.21 шивания исходящих Листинг 16.21. URL, Генерирование исходящих в файле Resul t. cshtml для запра­ которые нацелены на специальное представление. URL из специального класса маршрута Resul t. csh tml из папки Views/ Shared @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="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЬle-striped taЫe-sm"> <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аЫе> <а asp-route-legacyurl="/articles/Windows_З.1 class="Ьtn Ьtn-primary"> This is an out9oin9 URL </а> Overview.html" Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC 2 504 <р> URL: @Url.Action(null, null, new { legacyurl = 11 /articles/Windows_З.l OVerview.html" }) </р> </body> </html> В этом примере нет необходимости указывать вспомогательной функции дескрип­ тора контроллер и действие для исходящего маршрута, т.к. при генерации не применяются, а потому атрибуты asp-controller опущены. Когда генерируется только URL, го метода Url .Action () и asp-action URL они дескриптора а первые два аргумента для вспомогательно­ устанавливаются в null по той же причине. Если запустить приложение и просмотреть НТМL-разметку, полученную в ответ на запрос стандартного URL, то легко заметить, что для создания URL использовался специальный класс маршрута: <а class="Ьtn Ьtn-primary" href="/articles/windows_З.l_overview.html/"> This is an outgoing URL </а> <p>URL: /articles/windows_З.1 overview.html/</p> Завершающие косые черты, добавленные к true конфигурационного свойства URL, являются результатом установки в AppendTrailingSlash внутри файла Startup. cs, и важно. чтобы сопоставление входящего маршрута было способно соответствовать URL с добавленным символом косой черты. URL, который вы видите в НТМL-ответе, имеет отличающийся формат, такой как /? legacyur 1=%2Farticles %2FWindows _ 3. 1 _ Overv iew. html, то значит, что для генерации URL специальный маршрут не применялся, а вместо него был вызван какой­ Совет. Если то другой маршрут в приложении. Поскольку никакого контроллера или действия не было указано, произошло нацеливание на действие Index контроллера Home, и к строке за­ legacylJrl. В таком случае удостоверьтесь в установке свойства IsBound в true внутри метода GetVirtualPath (),и проверьте, что в кон­ фигурации внутри файла Startup. cs указаны корректные URL для конструктора класса LegacyRoute, а специальный маршрут определен перед всеми остальными маршрутами. проса URL добавилось значение Работа с областями Инфраструктура ASP.NEТ Соге МVС поддерживает организацию веб-приложения в виде областей, где каждая область представляет функциональный сегмент прило­ жения, такой как администрирование, выписка счетов-фактур, поддержка пользо­ вателей и т.д. Прием удобен в крупных проектах, в которых наличие единственно­ го набора папок для всех контроллеров, представлений и моделей может привести к сложностям в управлении. Каждая область MVC имеет собственную структуру папок, позволяя все хранить отдельно. Такой подход делает более очевидным то, какие элементы проекта относят­ ся к каждой части функциональности приложения, и помогает множеству разработ­ чиков трудиться над проектом, не конфликтуя друг с другом. Области в значительной степени поддерживаются системой маршрутизации и потому они рассматриваются вместе с URL и маршрутами. В настоящем разделе будет показано, как настраивать и пользоваться областями в проектах МVС. 505 Глава 16. Дополнительные возможности маршрутизации Создание области Создание области требует добавления папок в проект. Папка верхнего уровня на­ зывается Areas. Внутри нее для каждой необходимой области предусмотрена своя папка. содержащая собственные подпапки нируем создать область по имени санных в табл. ленные в табл. Таблица 16.6. 16.6. 16.6. Admin, Controllers, Views и Models. Мы пла­ что означает создание набора папок, опи­ Для подготовки примера проекта создадим все папки. перечис­ Папки, требуемые для подготовки областей Имя Описание Areas Эта папка будет содержать все области в приложении Areas/Admin Эта папка будет содержать классы и представления для области MVC Admin Areas/Admin/Controllers Эта папка будет содержать контроллеры для области Admin Areas/Admin/Views Эта папка будет содержать представления для области Admin Areas/Admin/Views/Home Эта папка будет содержать представления для контроллера Home в области Admin Areas/Admin/Models Эта папка будет содержать модели для области Admin Хотя каждая область применяется по отдельности, многие средства лагаются на стандартные функциональные возможности С# или .NET, MVC по­ такие как пространства имен. Чтобы облегчить использование области, первым делом нуж­ но добавить файл импортирования представлений, который позволит работать с моделями из области внутри представлений, не включая пространства имен и получая преимущество от применения вспомогательных функций дескрипторов. Создадим в папке ни_ View!mports. Листинг 16.22. Areas/Admin/Views файл импортирования представлений по име­ cshtml и поместим в него операторы, приведенные в листинге 16.22. Содержимое файла из папки _Viewimports. csh tml Areas/Admin/Views @using UrlsAndRoutes.Areas.Admin.Models @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Создание маршрута для области Чтобы задействовать области, потребуется добавить в файл который содержит переменную сегмента Листинг 16.23. (листинг UrlsAndRoutes System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Startup. cs 16.23). Добавление маршрута для областей в файле из папки using using using using using area Startup. cs маршрут, 506 Часть 11. Подробные сведения об инфраструктуре ASP.NET Соге MVC 2 using using using using using using Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Microsoft.AspNetCore.Routing.Constraints; Microsoft.AspNetCore.Routing; 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.UseDeveloperExceptionPage(); app.UseStatusCodePages(); app.UseStaticFiles(); app.UseMvc(routes => { арр, IHostingEnvironment env) { routes.МapRoute( name : "areas" , template: "{area:exists}/{controller=Home}/{action=Index}"); routes.Routes.Add(new LegacyRoute( app.ApplicationServices, "/articles/Windows_З.l_Overview.html", "/old/.NET 1.0 Class Library")); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "out", template: "outbound/{controller=Home}/{action=Index}"); }) ; Переменная сегмента area используется для сопоставления с URL, которые наце­ лены на контроллеры в специфических областях. В листинге задействован стандар­ тный шаблон URL, но сегмент area можно добавлять в любой требующийся шаблон. Маршрут, который добавляет поддержку областей, должен находиться перед менее специфическими маршрутами для обеспечения корректного сопоставления с Ограничение exist:;; URL. применяется для гарантии того, что запросы соответствуют только областям, которые были определены в приложении. Заполнение области Контроллеры, представления и модели в области можно создавать точно так же, как в главной части приложения кнопкой мыши на папке MVC. Чтобы создать модель, щелкнем правой Areas/Admin/Models, выберем в контекстном меню пункт 507 Глава 16. Дополнительные возможности маршрутизации AddqClass (Добавитьqf\Jшсс) и создадим файл мое которого показано в листинге Листинг 16.24. Содержимое файла IUiacca по имени Person. cs, содержи­ 16.24. Person. cs из папки Areas/Admin/Models namespace UrlsAndRoutes.Areas.Admin.Models puЫic class Person { puЫic string Name { get; set; puЫic string Ci ty { get; set; Для создания контроллера щелкнем правой кнопкой мыши на папке Areas/Admin/ Controllers. выберем в контекстном меню пункт AddqClass и создадим файл 1Uiacca по имени HorneController. cs с определением контроллера из листинга 16.25. Листинг 16.25. Содержимое файла из папки HomeController. cs Areas/Admin/Controllers using Microsoft.AspNetCore.Mvc; using UrlsAndRoutes.Areas.Admin.Models; namespace UrlsAndRoutes.Areas.Admin.Controllers [Area("Admin")] class HomeController : Controller { private Person[] data = new Person[] { "Alice", Ci ty = "London" } , new Person { Name "ВоЬ", City = "Paris" }, new Person { Name "Joe", City = "New York" new Person { Name puЫic }; puЫic ViewResult Index() => View(data); Новый контроллер в целом похож на стандартный кроме одного аспекта. Чтобы ас­ социировать контроллер с областью. к 1Uiaccy должен быть применен атрибут Area: [Area ( "Admin") ] puЬlic class HomeController : Controller { Без атрибута Area контроллеры не будут принадлежать области, даже если они не определены в главной части приложения. Отсутствие атрибута Area может привести к странным результатам. Это первое, что следует проверять. если при работе с облас­ тями получаются не те результаты, которые ожидались. Совет. Если для настройки маршрутов используются атрибуты, как было описано в главе 15, Route можно ссы­ латься на область, указанную с помощью атрибута Area: [ Route (" [ area] / арр/ [controller]/actions/[action]/{id:weekday?}")]. тогда с применением маркера Наконец, добавим в папку по имени Index. cshtrnl, [area] в аргументе атрибута Areas/Admin/Views/Horne файл представления содержимое которого показано в листинге 16.26. Razor 508 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 Листинг 16.26. Содержимое файла Index. cshtml из папки Areas/Admin/Views/Home @model Person[ ] @( Layout = null; < 1 DOCTYPE html> <html> <head> <meta n a me="vi e wport" c on tent=" width=de v i c e -w idt h" / > <t i tle >Ar eas< / tit le> <link rel="styles heet" asp-href-in c l ude = "liЬ/boo tstrap /di st /css / *.m in .css" / > </he ad> <body class="m-1 р-1"> <tаЫе class="t a Ыe taЫe-b order ed taЫe- s t ri p ed taЫe- s m" > <tr><th>Name </th>< th>City< / th>< /tr> @foreach (Pe rson р in Mo d e l ) { <tr >< td >@p . Name < / td >< td>@p .C ity</ td >< / t r> < / tаЫе> </ body> < / html> Моделью для данного представления является массив объектов Pe r s on . Благодаря файлу импортирования представлений. содержимое которого бьvю приведено в листин­ ге 16.22, есть возможность ссылаться на тип Запуск приложения и запрос Person без указания проС'транства имен. URL вида / Adrnin даст результат. Влияние области на приложение показанный на рис. 16.8. MVC Важно понимать влияние, которое области оказывают на остальные части приложения. Мы создали область по имени Adrniп, но в главной части приложения имеется также контрол­ Adrnin . До создания области запрос /Admin направлялся бы действию Index конт ­ Adrnin в главной части приложения; теперь он будет нацелен на действие Inde x контроллера Ноте в области Adrnin (корень области предоставляет стандартные значения лер роллера для переменных сегментов contr o l ler и action). Изменение такого вида может стать причиной неожиданного поведения, и наилучший способ применения областей предусмат­ ривает встраивание их использования в первоначальную схему именования контроллеров для проекта. Если возникла потребность добавить области в готовое приложение, то вы должны тщательно обдумать их влияние на имеющиеся маршруты. Name Clty Alice london ВоЬ Paris Joe NewYork Рис. 16.8. Использование области Глава 16. Дополнительные возможности маршрутизации 509 Генерирование ссылок на действия в областях Для создания ссылок, указывающих на действия в той же области, к которой от­ носится текущий запрос, никаких дополнительных шагов предпринимать не придет­ ся. Инфраструктура MVC обнаруживает, что текущий запрос относится к конкретной области, и гарантирует, что средство генерации исходящих URL будет искать соот­ ветствие только среди маршрутов, определенных для данной области. Например, в 16.27 листинге папки демонстрируется добавление элемента а в файле Index. cshtml из Areas/Admin/Views/Home. Листинг 1б.27. Добавление элемента а в файле из папки Index. csh tml Areas/Admin/Views/Home @model Person[J @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-wid th" /> <title>Areas</title> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> </head> <body class="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЬle-sm"> <tr><th>Name</th><th>Cit y</th></tr> @foreach (Person р in Model) { <tr><td>@p.Name</td><td> @p.City</td></tr> </tаЫе> <а asp-action="Index" asp-controller="Home">Lin k</a> </body> </html> Запустив приложение и запросив URL вида / admin, можно заметить, что ответ содержит следующий элемент: <а href="/admin/">Link</a> При генерации исходящей ссылки система маршрутизации выбрала маршрут для области и учла стандартные значения, доступные для переменных сегментов controller и action. Чтобы создать ссылку на действие в другой области или в главной части прило­ жения, систему маршрутизации необходимо снабдить значением для сегмента (листинг Листинг 16.28). 16.28. Нацеливание на другую область в файле из папки @model Person [] @{ Layout = null; <!DOCTYPE html> <html> Areas/Admin/Views/Home Index.cshtml area Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 510 <head> <meta name="viewport" content="width=device-width" /> <title>Areas</title> <link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" /> </head> <body class="m-1 р-1"> <tаЫе class="taЫe taЫe-bordered taЫe-striped taЫe-sm"> <tr><th>Name</th><th>City</th></tr> @foreach (Person р in Model) { <tr><td>@p.Name</td><td>@p.City</td></tr> </tаЫе> <а asp-action="Index" asp-controller="Home">Link</a> <а asp-action="Index" asp-controller="Home" asp-route-area="">Link</a> </body> </html> Атрибут asp-route-area устанавливает значение для переменной сегмента area. В данном случае эта переменная устанавливается в пустую строку. что указывает на главную часть приложения и дает следующий НТМL-элемент: <а href="/">Link</a> Если вы располагаете в своих контроллерах множеством областей и хотите марш­ рутизировать на них. тогда вместо пустой строки используйте имя нужной области. Полезные советы относительно схемы URL После всего сказанного у вас может все же остаться вопрос: с чего начинать про­ ектирование собственной схемы URL? Вы можете просто принять стандартную схему. но организация собственной схемы обеспечивает ряд преимуществ. В последние годы URL проектное решение с приложения применяется все более широко, и появилось несколько важных принципов проектирования. Следуя этим принципам проектиро­ вания. вы улучшите удобство использования, совместимость и поисковый рейтинг своих приложений. Делайте URL чистыми и понятными человеку Пользователи обращают внимание на URL в приложениях. Просто вспомните, ког­ да вы в последний раз пытались отправить кому-то выглядит URL для URL веб-сайта Amazon. Вот как предыдущего англоязычного издания настоящей книги: https://www.amazon.com/Pro-ASP-NET-Core-ADAМ-FREEMAN/dp/1484203984 Отправить по электронной почте такой URL еще реально. но попробуйте продикто­ вать его по телефону. Когда подобной работой приходится заниматься часто. в итоге проще сообщить номер на веб-сайте URL Amazon. ISBN книги и предложить самостоятельно найти ее страницу Было бы неплохо получать доступ к странице книги с помощью следующего вида: http://www.amazon.com/books/pro-asp-net-core Такой URL вполне можно было бы диктовать по телефону. и он не выглядит так. будто при наборе почтового сообщения на клавиатуру что-то свалилось. 511 Глава 16. Дополнительные возможности маршрутизации На заметку! В качестве пояснения: я глубоко уважаю компанию Amazoп, которая продает моих книг больше, чем все остальные вместе взятые. Мне известен факт, что все сотруд­ ники Amazoп - замечательно умные и приятные люди. Никто из них не окажется до такой степени мелочным, что прекратит продажи моих книг из-за настолько несущественного, как критика применяемого в Amazoп формата URL. Я люблю Amazoп. Я преклоняюсь пе­ URL. ред Amazoп. Я просто хочу, чтобы там привели в порядок свои Ниже даются руководящие принципы построения дружественных к пользователям URL. • Проектируйте URL так, чтобы они описывали предоставляемое содержимое, а не детали реализации приложения. Используйте не • /Articles/AnnualReport. /Website v2/CachedContentServer/FromCache/AnnualReport. а Отдавайте предпочтение заголовкам содержимого перед идентификационными номерами. Применяйте /Articles/AnnualReport. а не /Articles/2392. Если вы должны использовать идентификационные номера (для различения элемен­ тов с одинаковыми заголовками или во избежание дополнительных запросов к базе данных. необходимых для поиска элемента по его заголовку). тогда указы­ вайте и номер. и заголовок (например. URL дольше /Articles/2392/AnnualReport). Такой набирать, но он имеет больше смысла для человека и улучшает по­ исковый рейтинг. Приложение может просто игнорировать заголовок и отобра­ жать элемент, соответствующий идентификатору. • Не применяйте расширения имен файлов для НТМL-страниц (скажем, или как . aspx .mvc), но используйте их для специализированных типов файлов (таких . jpg .. pdf и . zip). Веб-браузеры не придают значения расширениям имен файлов. если тип скажем, файлов • MIME PDF установлен корректно. но люди ожидают, что имена, заканчиваются на .pdf. Создавайте осмысленную иерархию (например, /Products/Menswear /Shirt.s/Red), чтобы посетитель смог догадаться, как выглядит • URL родительской категории. Поддерживайте независимость от регистра символов. (Кто-то может пожелать ввести URL. указанный на печатной странице.) По умолчанию система ASP.NET Core не чувствительна к регистру символов. марш­ рутизации • Избегайте применения знаков, кодов и символьных последовательностей. Если вам нужен разделитель слов, то используйте дефис (как в Подчеркивания не являются дружественными кодированные пробелы выглядят либо странно к /my-great-article). пользователю, (/my+grea t +art icle), а URL- либо плo­ xo (/my% 20great %20arti cl~. • Не изменяйте URL. Неработающие ссылки равнозначны потерям в бизнесе. URL продолжайте поддерживать старую схему URL насколько После изменения возможно долго посредством перенаправления. • Поддерживайте согласованность. Примите один формат URL в масштабах всего приложения. URL должны быть короткими, легкими для набора. редактируемыми человеком и постоянными. к тому же они обязаны отражать структуру сайта. Якоб Нильсен. гуру по удобству использования, развивает эту тему по адресу https: / /www. nngroup. сот/ articles/url-as-ui/. ТИм Бернерс-Ли, изобретатель веб-сети. предлагает анало­ гичные советы (https: / /www. w3. org/Provider/Style/URI). 512 Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 GET и POST: выбор правильного запроса Эмпирическое правило гласит, что запросы GET должны применяться для извлече­ ния информации, предназначенной только для чтения, а запросы POST - для любой операции, которая изменяет состояние приложения. В терминах соответствия стан­ дартам запросы GET предназначены для безопасных взаимодействий (не имеющих побочных эффектов помимо извлечения информации), а запросы POST - для небе­ зопасных взаимодействий (принятие решения либо изменение чего-нибудь). Такие соглашения установлены консорциумом WЗС (World Wlde Web Coпsortlum) и описаны по адресу: http://www.w3.org/Protocols/rfc2616/rfc2616-sec 9.html Запросы GET являются адресуемыми: вся информация содержится в URL. а пото­ му подобные адреса можно добавлять в закладки и ссылаться на них в будущем. Не используйте запросы GET для операций, которые изменяют состояние. Многие разработчики веб-приложений научились этому на горьком опыте в бьmо выпущено приложение Google Web Accelerator. 2005 году. когда Оно с упреждением выбирало все содержимое. на которое ссылалась каждая страница. что в рамках протокола НТТР вполне законно, поскольку запросы GET должны быть безопасными. К сожалению, многие разработчики веб-приложений проигнорировали соглашения НТТР и разме­ щали в своих приложениях простые ссылки на операции типа "удалить элемент" или "добавить в корзину". В результате получился хаос. В одной компании были уверены, что их система управления содержимым стала целью неоднократных атак со стороны злоумышленников, т.к. все содержимое пос­ тоянно удалялось. Позже в компании обнаружили, что поисковый агент наткнул­ ся на URL административной страницы и прошелся по всем ссылкам на удаление. Аутентификация может защитить от таких действий со стороны пользователей, но не способна обеспечить защиту от веб-акселераторов. Резюме В главе рассматривались дополнительные возможности системы маршрутизации. Вы узнали, как генерировать исходящие ссылки и URL, а также научились настраи­ вать систему маршрутизации. По ходу дела вы ознакомились с концепцией областей и с рекомендациями относительно того, как должна создаваться удобная и содержа­ тельная схема URL. В следующей главе мы перейдем к исследованию контроллеров и действий, которые являются центральной частью инфраструктуры ASP.NET Core МVС. Будет подробно описана их работа и показано, как их применять для получения на­ илучших результатов в приложении. ГЛАВА 17 Контроллеры и действия в приложение, обрабатывается контроллером. к аждый запрос. который поступает .NET, Core В инфраструктуре ASP.NEТ МVС контроллеры являются классами рые содержат логику, требующуюся для обработки запроса. В главе 3 кото­ объяснялось, что роль контроллера заключается в инкапсуляции логики приложения. Это значит, что контроллеры отвечают за обработку входящих запросов, вьшолнение операций над мо­ делью предметной области и выбор представлений для визуализации пользователю. Контроллер волен обрабатывать запрос любым способом, какой посчитает подхо­ дящим, до тех пор, пока он не забирается в области ответственности, которые от­ носятся к модели и представлению. Таким образом, контроллеры не содержат и не сохраняют данные, а также не генерируют пользовательские интерфейсы. В настоящей главе мы рассмотрим реализацию контроллеров, а также различные способы применения контроллеров для получения и генерации вывода. В табл. 17.1 приведена сводка, позволяющая поместить контроллеры в контекст. Таблица 17. 1. Помещение контроллеров в контекст Вопрос Ответ Что это такое? Контроллеры содержат логику для получения запросов, обновле­ ния состояния или модели приложения и выбора ответа, который будет отправлен клиенту Чем они полезны? Контроллеры являются центральной частью проектов MVC и со­ держат логику предметной области для веб-приложения Как они используются? Контроллеры - это классы С#, открытые методы которых вызывают­ ся для обработки НТТР-запроса. Методы могут брать на себя ответс­ твенность за выдачу ответа клиенту напрямую, но более распростра­ ненный подход предусматривает возвращение результата действия, который сообщает MVC, как ответ должен быть подготовлен - новичок в области разработки приложений MVC, то Существуют ли какие­ Если вы то скрытые ловушки можете легко создавать контроллеры, содержащие функциональ­ или ограничения? ность, которая больше подходит для модели или представления. Более специфичная проблема связана с тем, что любые открытые Controller, инфра­ MVC считает контроллерами; это означает возможность классы с именами, заканчивающимися на структура непредумышленной обработки НТТР-запросов в классах, которые не планировались быть контроллерами Существуют ли альтернативы? Нет, контроллеры - основная часть приложений MVC 514 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 В табл. Таблица 17.2 17.2. приведена сводка по главе. Сводка по главе Задача Решение Определение контроллера Листинг Создайте открытый класс с именем, Controller, или класса Controller заканчивающимся на унаследуйте его от Получение деталей НТТР-запроса Используйте объекты контекста или оп- Выдача результата из метода действия 17.717.9 ределите параметры методов действий 17.1017.13 Работайте напрямую с объектом контекста результата или создайте объект 17.1417.16 результата действия Выдача НТМL-результата Создайте результат представления 17.1717.24 Перенаправление клиента Создайте результат перенаправления 17.2517.30 Возвращение содержимого клиенту Создайте результат содержимого 17.3117.35 Возвращение кода СОСТОЯНИЯ НТТР Создайте НПР-результат 17.36, 17.37 Подготовка проекта для примера Для целей главы мы создадим новый проект типа ControllersAndActions с применением шаблона Empty (Пустой) по имени ASP.NET Core Web Applicatioп (.NET Core) (Веб-приложение ASP.NET Соге (.NET Core)). Добавим в класс Startup операто­ ры, чтобы включить инфраструктуру MVC и другие компоненты промежуточного ПО (листинг 17.1). На заметку! Данная глава содержит модульные тесты для основных средств. Для краткости проекты модульного тестирования в инструкции по созданию примера проекта не включе­ ны. Вы можете создавать тестовые проекты, следуя процессу, который описан в главе или загрузить готовый проект SportsStore из хранилища GitHub 7, для настоящей книги (https: / /gi thub. com/apress/pro-asp .net-core-mvc-2). Листинг 17 .1. Добавление инфраструктуры ПО в файле using using using using using using using using Startup. cs MVC и других компонентов промежуточного из папки ControllersAndActions System; System.Collections.Generic; System.Linq; System.Threading.Tasks; Microsoft.AspNetCore.Builder; Microsoft.AspNetCore.Hosting; Microsoft.AspNetCore.Http; Microsoft.Extensions.Dependencyinjection; Глава 17. Контроллеры и действия 515 namespace ControllersAndActions { puЫic class Startup { puЫic void ConfigureServices(IServiceCollection services) { services.AddМvc(); services.AddМeшoryCache(); services.AddSession(); puЫic void Configure(IApplicationBuilder арр, IHostingEnvironment env) { app.UseStatusCodePages(); app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseSession(); app.UseМvcWithDefaultRoute(); ) Методы AddMemoryCache () и AddSession () создают службы. необходимые для UseSession () добавляет в конвейер компонент проме­ управления сеансом. Метод жуточного ПО, который ассоциирует данные сеанса с запросами, и добавляет сооkiе­ наборы к ответам. гарантируя возможность идентификации будущих запросов. Метод UseSession () должен вызываться перед вызовом метода UseMvc (),чтобы компо­ нент сеанса мог перехватывать запросы, прежде чем они достигнут промежуточного ПО MVC. и модифицировать ответы после того, как они были сгенерированы. Другие методы настраивают стандартные пакеты, которые рассматривались в главе 14. Подготовка представлений Основное внимание в главе уделяется контроллерам и их методам действий. поэ­ тому классы контроллеров будут определяться в главе повсеместно. В качестве под­ готовки будут определены представления, которые помогут демонстрировать работу контроллеров и методов действий. Созданные в текущем разделе представления на­ ходятся в папке Views/Shared, что позволит их использовать в любых контроллерах, которые будут определяться позже в главе. Создадим папку нее файл представления Razoг по имени ку. показанную в листинге Листинг 17.2. Resul t. cshtml Views/Shared, добавим в и поместим в файл размет­ 17.2. Содержимое файла Result. cshtшl из папки Views/Shared @model string @{ Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Controllers and Actions</title> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> </head> <body class="m-1 р-1"> Model Data: @Model </body> </html> Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 516 Моделью для данного представления является объект типа лит отображать простые сообщения. Далее создадим в папке имени тинге DictionaryResul t. csh tml 17.3. string. который позво­ Views/Shared файл по и поместим в него разметку, приведенную в лис­ В качестве модели для этого представления выбран словарь. который даст возможность отображать более сложные данные, чем в предыдущем представлении. Листинг 17 .3. Содержимое файла из папки DictionaryResul t. csh tml Views/Shared @model IDictionary<string, string> @( Layout = null; 1 <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Controllers and Actions</title> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/*.min.css" /> </head> <body class="m-1 р-1 "> <tаЫе class="taЫe taЬle-bordered taЫe-sm taЬle-striped"> <tr><th>Name</th><th>Value</th></tr> @foreach (string key in Model.Keys} ( <tr><td>@key</td><td>@Model[key]</td></tr> </tаЫе> </body> </html> Затем создадим в папке Views/Shared файл по имени SimpleForm. cshtml и оп­ ределим в нем представление, показанное в листинге 17.4. Как следует из названия. представление содержит простую НТМL-форму, которая будет получать данные от пользователя. Листинг 17.4. Содержимое файла SimpleForm. cshtml из папки Views/Shared @( Layout = null; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Controllers and Actions</title> <link rel="stylesheet" asp-href-include="liЬ/bootstrap/dist/css/* .min.css" /> </head> <body class="m-1 р-1"> <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-group"> <label for="name">City:</label> <input class="form-control" name="city" /> </div> Глава 17. Контроллеры и действия <button class="btn btn-prirnary </button> </forrn> </body> </htrnl> 517 type="subrnit">Subrnit center-Ыock" В представлениях используются встроенные вспомогательные функции дескрип­ торов для генерации URL из системы маршрутизации. Чтобы включить вспомогатель­ ные функции дескрипторов, создадим в папке лений по имени_ Viewimports. листинге Листинг cshtrnl Views файл импортирования представ­ и добавим в него выражение, приведенное в 17.5. Содержимое файла_Viewimports. 17.5. cshtml из папки Views @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers Все представления. созданные в папке Views/Shared, зависят от пакета имени Bootstгap. Для добавления Bootstгap в проект создадим файл Bower Configuration File (листинг 17.6). применением шаблона элемента и добавим пакет Bootstгap Листинг 17 .6. Добавление пакета в файле CSS по bower. j son с (Файл конфигурации Воwег) bower. j son из папки ControllersAndActions "name": "asp.net", "private": true, "dependencies": { "bootstrap": "4. О. 0-alpha. 6 11 Понятие контроллеров Контроллеры - это классы С#. открытые методы которых (известные как методы действий или просто действия) несут ответственность за обработку НТГР-запроса и подготовку ответа, возвращаемого клиенту. Для определения. какой класс контрол­ лера и метод действия должны обработать запрос, инфраструктура МVС использует систему маршрутизации. описанную в главах 15 и 16. Затем она создает новый эк­ земпляр класса контроллера. вызывает метод действия и применяет результат метода для выпуска ответа клиенту. Инфраструктура MVC снабжает методы действий данными контекста, так что они могут выяснить, каким образом обрабатывать запрос. Доступен широкий спектр данных контекста, который описывает все, что касается текущего запроса. подготав­ ливаемого ответа, извлеченных системой маршрутизации данных и деталей идентич­ ности пользователя. Когда MVC вызывает метод действия. ответ из метода описывает ответ, который должен быть послан клиенту. Самый распространенный вид ответа создается путем визуализации представления Razoг, поэтому метод действия использует свой ответ, чтобы сообщить MVC, какое представление задействовать и какие данные модели представления ему предоставить. Но имеются также другие виды ответов, и методы Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 518 действий могут делать все, что угодно, от требования у MVC отправки клиенту пере­ направления НТГР до отправки сложных объектов данных. Таким образом, существуют три области функциональности, которые важны для понимания контроллеров. Во-первых, нужно понимать особенности определения кон­ троллеров, чтобы инфраструктура MVC могла применять их для обработки запросов. Контроллеры представляют собой просто классы С#. но создавать их можно разными способами, и важно уяснить отличия между ними. Определение контроллеров рас­ сматривается в разделе "Создание контроллеров" далее в главе. Во-вторых, следует понимать. каким образом МVС снабжает методы действий дан­ ными контекста. Получение необходимых данных контекста важно для эффективной разработки веб-приложений, а инфраструктура MVC облегчает его, определяя набор классов, которые используются с целью описания всего, что требуется тому или ино­ му методу действия. В разделе "Получение данных контекста" позже в главе объясня­ ется, как MVC описывает запросы и ответы. Наконец, в-третьих, важно понимать, каким образом методы действий выпускают ответ. Методы действий редко нуждаются в самостоятельной генерации НТГР-ответа, и вы должны знать, каким образом инструктировать MVC о необходимости выпус­ ка требуемых ответов, что будет показано в разделе "Генерирование ответа" далее в главе. Создание контроллеров Вы сталкивались с применением контроллеров почти во всех предшествующих главах. Наступило время заглянуть "за кулисы" и посмотреть, как они определяются. В последующих разделах будут описаны различные способы создания контроллеров и приведены объяснения отличий между ними. Создание контроллеров РОСО Инфраструктура MVC поддерживает соглашение по конфигурации, а это значит. что контроллеры в приложении МVС обнаруживаются автоматически вместо того, чтобы определяться в конфигурационном файле. Базовый процесс обнаружения прост: любой класс puЫic с именем, заканчивающимся на Controller, является контроллером. а - действием. Чтобы выяснить, как все ра­ ботает, добавим в проект папку Controllers и поместим в нее файл класса по имени PocoController. cs. содержимое которого приведено в листинге 17.7. любой определенный в нем метод puЫic Совет. Хотя соглашение предусматривает помещение контроллеров в папку они могут находиться где угодно в проекте и Листинг 17.7. Содержимое файла MVC PocoController.cs из папки namespace ControllersAndActions.Controllers puЫic class PocoController { puЫic string Index () => "This is а Controllers, по-прежнему отыщет их. РОСО controller"; Controllers Глава 17. Контроллеры и действия Класс PocoController удовлетворяет простому критерию. который MVC 519 ожида­ ет обнаружить в контроллере. Он определяет единственный метод p uЫic по имени Index (), который будет использоваться как метод действия и который string. Класс PocoC o n tr oller является примером контроллера РОСО, где возвращает объект чает ("простой старый объект "plain olcl CLR object" CLR") РОСО озна­ и опирается на тот факт. что контроллер реализован с применением стандартных средств .NET без какой-либо прямой зависимости от АРl-интерфейса. предоставляемого инфраструктурой ASP.NET Core MVC. Чтобы протестировать контроллер РОСО, запустим приложение и запросим URL вида /Рос о/ Index /. Система маршрутизации сопоставит запрос со стандартным шаб­ лоном URL и направит запрос методу r\ locolhost62433/Poco;'lr С I ndex () класса PocoControlle r (рис. 17.1). Х (j) localhost ~4 3/Poco/lnd>:x This is а РОСО controller Рис. Использование контроллера РОСО 17. 1. Применение атрибутов для корректировки идентификации контроллеров Поддержка контроллеров РОСО не всегда работает желательным образом. Общая проблема заключается в том, что MVC будет идентифицировать в качестве контроллеров фиктивные классы, созданные для модульного тестирования. Избежать указанной проблемы проще всего, уделяя внимание именам классов и не допуская имен вроде FakeContro l l er. Если NonCont ro l l er, опреде­ ленный в пространстве имен Micr o s o ft .AspNetCore . Mvc , чтобы сообщить MVC о том, что класс не является контроллером. Имеется также атрибут NonAction, который может это невозможно, тогда к классу необходимо применять атрибут применяться к методам, чтобы предотвратить их использование как методов действий. В ряде проектов может отсутствовать возможность следования соглашению об именовании для класса , который должен выступать в качестве контроллера РОСО. Инфраструктуре MVC можно указать, что класс является контроллером, даже когда он не удовлетворяет критерию выбора РОСО, применив атрибут имен Contro ller, Microsoft. AspNetCore. Mvc. который также определен в пространстве Использование АР/-интерфейса контроллеров инфраструктуры Класс PocoControlle r - троллеров инфраструктурой MVC удобная демонстрация способа иде нтификации кон ­ MVC и возможной реализации простых контроллеров. Но чистые контроллеры РОСО. не имеющие зависимостей от пространств имен Microsoft. Aspn e t Core. средствам. которые MVC не особенно полезны, поскольку у них отсутствует доступ к предлагает для обработки запросов. Часть 11. Подробные сведения об инфраструктуре ASP.NET Саге MVC 2 520 Доступ к определенным частям АРI-интерфейса MVC может осуществляться за счет создания новых экземпляров Юlассов из пространств имен Mi c rosot"t . AspnetCore. В качестве простого примера Ю1асс РОСО может затребовать у MVC визуализацию ViewResu l t из своих методов действий, ViewResu l t будет рассматриваться в разделе представления Razoг, возвращая объект как показано в листинге 17 .8. (Класс "Генерирование ответа" далее в главе.) Листинг 17.8. Использование АРl-интерфейса из папки using using using MVC в файле PocoController. cs Controllers Мicrosoft. AspNetCore. Mvc; Мicrosoft.AspNetCore.Mvc . ModelBinding; Мicrosoft.AspNetCore.Mvc.ViewFeatures; namespace ControllersAndActions.Controllers puЫi c class PocoController { puhlic ViewResul t Index () => new ViewResul t () ViewName "Result", ViewData new ViewDataDictionary( new EmptyМodelМetadataProvider(), new ModelStateDictionary()) { Model = $"This is а РОСО controller" = = } }; Класс P ocoCo пtroller больше не является чистым контроллером РОСО. т.к. он имеет прямые зависимости от АРI-интерфейса MVC. Однако. оставив в стороне чис ­ тоту, данный Ю1асс намного более полезен , чем Ю1асс из предыд.vщего примера. по ­ Razor. К сожалению, ViewResul t, необходимо создать объекты View DataDictionary. EmptyModelMet adataF'rov ide r и ModelSta teDict ionar y. тому что он запрашивает у МVС визуализацию представления код стал сложным. Чтобы создать объект которые требуют доступа к трем разным пространствам имен. (Средства , к которым относятся упомянутые типы, будут описаны в последующих главах.) Суть рассматри ­ ваемого приме ра - демонстрация того . что к средствам. предлагаемым MVC. можно обращаться напрямую. даже если в результате получается некоторая путаница. 17.8 обеспечивают визуализацию stri ng в качестве модели пред­ ставления. В результате запуска приложения и запрашивания URL вида / F'oco / I пdе х будет получен ответ. приведенный на рис. 17.2. Выделенные полужирным изменения в листинге представления Resul t. cshtml D Conltol!~rs •nd ActJons с применением типа >< Model Data: Тf1is is а РОСО controller Рис. 17.2. Работа с АРl-интерфейсом MVC напрямую Глава 17. Контроллеры и действия Использование базового класса 521 Con troller Предшествующие примеры проиллюстрировали. каким образом начать с контрол­ лера РОСО и обеспечить ему доступ к средствам на то, как работает MVC: MVC. Такой подход проливает свет это полезное знание на тот случай. если вы обнаружите. что невольно создаете контроллеры. но контроллеры РОСО неудобны в написании. восприятии и сопровождении. Более легкий способ создания контроллеров предусматривает наследование клас­ сов от класса Microsoft.AspNetCore.Mvc.Control ler . тоды и свойства, предоставляющие доступ к средствам нере. Добавим в папку Controller s файл класса по имени с определением контроллера. показанным в листинге Листинг 17.9. в котором определены ме­ MVC в сжатой и удобной ма­ DerivedContro ller. cs 17.9. Наследование от класса Controller в файле DerivedController. cs из папки Controllers using Microsoft.AspNetCore.Mvc; namespace ControllersAndActions. Controllers puЫic class DerivedController : Controller ViewResult Index() => View("Result", $"This is а derived controller"); puЫic На рис. 17.3 приведен / Derived/Index. результат запуска приложения и запрашивания D Contto'~rs and Actions С URL вида Х ф localhost »2433/Derived/lnd<:!x Model Data: This is а derived controller Рис. 17.3. Контроллер в листинге Использование базового класса 17.9 делает (он требует у инфраструктуры ставления st.ring). MVC Controller то же самое. что и контроллер в листинге 17.8 визуализировать представление с моделью пред­ но применение базового класса Cont roller означает возмож­ Vi ewRe s u 1 t, требующийся для ность получения результата более простым путем . Основное изменение касается того. что объект визуализации представления Razor. можно создать с использованием метода Vi e w () вместо явного создания экземпляра этого класса (и необходимых ему других типов) прямо в методе действия. Метод а объект ViewResul t View () унаследован от базового класса Cont roller. по-прежнему создается тем же способом, просто не загромож­ дая метод действия. Наследование от класса Controlle r не изменяет манеру работы контроллеров: оно всего лишь упрощает код , который пишется для решения общих задач. 522 Часть 11. Подробные сведения об инфраструктуре ASP.NET Core MVC 2 На заметку! Инфраструктура MVC создает новый экземпляр класса контроллера для каждо­ го запроса, который ей предлагается обработать. В итоге не придется синхронизировать доступ к методам действий или свойствам и полям экземпляра. Разделяемые объекты, в том числе базы данных и службы-одиночки, которые описаны в главе 18, могут приме­ няться параллельно и должны быть соответствующим образом реализованы. Получение данных контекста Независимо от того. как определяются контроллеры. они редко будут существовать в изоляции и обычно нуждаются в доступе к данным из входящего запроса, таким как значения строки запроса. значения формы и параметры, извлеченные из URL системой маршрутизации. что все вместе называют данными контекста. Есть три основных способа доступа к данным контекста: • извлечение данных из набора объектов контекста; • получение данных как параметра метода действия; • явное обращение к средству привязки моделей инфраструктуры. Здесь мы рассмотрим подходы к получению входных данных для методов дейс­ твий, сосредоточившись на объектах контекста и параметрах методов действий. Привязка моделей раскрывается в главе 26. Получение данных из объектов контекста Одно из главных преимуществ использования базового класса Controller для создания контроллеров заключается в удобном доступе к набору объектов контекста. которые описывают текущий запрос, подготавливаемый ответ и состояние приложе­ ния. В табл. 17.3 описаны наиболее полезные свойства контекста Таблица 17.З. Полезные свойства класса Controller. Controller для данных контекста Имя Описание Request Это свойство возвращает объект HttpRequest, 17.4) который описывает за­ прос, полученный от клиента (табл. Response Это свойство возвращает объект создания ответа клиенту (табл. Нt t pCon tex t HttpResponse, 17.7) Это свойство возвращает объект н t tpCon tex t, который применяется для который является источником многих объектов, возвращаемых другими свойствами, такими как Response. Reque s t и Оно также предоставляет информацию о доступных возможностях НТТР и доступ к низкоуровневым средствам наподобие веб-сокетов RouteData Это свойство возвращает объект RouteData, выпускаемый системой марш­ рутизации, когда имеется совпадение с запросом, как описано в главах ModelState 15 и 16 Это свойство возвращает объект ModelStateDictionary, который исполь­ зуется для проверки достоверности данных, отправленных клиентом (глава User Это свойство возвращает объект ClaimsPrincipal, 27) который описывает пользователя, сделавшего запрос, как объясняется в главах Многие контроллеры пишутся без применения свойств из табл. 29 17.3. и 30 потому что данные контекста доступны через средства. рассматриваемые в последующих главах. 523 Глава 17. Контроллеры и действия которые больше гармонируют со стилем разработки приложений МVС. Например. большинство контроллеров не нуждаются в использовании свойства Request для получения деталей обрабатьшаемого НТТР-запроса, поскольку та же самая информация доступна через процесс привязки модели, который обсуждается в главе Но свойства, перечисленные в табл. 17